From 48c6bb78bc22b3c69f5a3f3f471dec6092a3bdab Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 9 Jul 2019 17:21:53 -0600 Subject: [PATCH] Removes essentials-framework as a submodule and brings the files back into the main repo --- .gitignore | 3 +- .gitmodules | 3 - essentials-framework | 1 - .../PepperDashEssentialsBase.sln | 20 + .../Comm and IR/CecPortController.cs | 100 + .../Comm and IR/ComPortController.cs | 132 ++ .../Comm and IR/ComSpecJsonConverter.cs | 102 + .../Comm and IR/CommFactory.cs | 196 ++ .../Comm and IR/CommunicationExtras.cs | 23 + .../Comm and IR/ConsoleCommMockDevice.cs | 74 + .../Comm and IR/DELETE ComPortController.cs | 105 + .../Comm and IR/GenericComm.cs | 56 + .../Comm and IR/IRPortHelper.cs | 160 ++ .../Config/BasicConfig.cs | 40 + .../Config/ConfigPropertiesHelpers.cs | 30 + .../Config/DeviceConfig.cs | 65 + .../Config/Essentials/ConfigReader.cs | 176 ++ .../Config/Essentials/ConfigUpdater.cs | 223 ++ .../Config/Essentials/ConfigWriter.cs | 163 ++ .../Config/Essentials/EssentialsConfig.cs | 67 + .../Config/InfoConfig.cs | 96 + .../SourceDevicePropertiesConfigBase.cs | 13 + .../Constants/CommonCues.cs | 127 + .../Crestron IO/IOPortConfig.cs | 19 + .../Inputs/GenericDigitalInputDevice.cs | 43 + .../Inputs/GenericVersiportInputDevice.cs | 50 + .../Crestron IO/Inputs/IDigitalInput.cs | 16 + .../Crestron IO/Relay/GenericRelayDevice.cs | 69 + .../Crestron IO/Relay/ISwitchedOutput.cs | 26 + .../Crestron/CrestronGenericBaseDevice.cs | 156 ++ .../CrestronGenericBaseDevice.cs.orig | 128 + .../Cues and DevAction/Cues.cs | 98 + .../IPresentationSource.cs | 19 + .../DeviceTypeInterfaces/IChannel.cs | 47 + .../DeviceTypeInterfaces/IColorFunctions.cs | 41 + .../DeviceTypeInterfaces/IDPad.cs | 50 + .../IDiscPlayerControls.cs | 13 + .../DeviceTypeInterfaces/IDisplayBasic.cs | 22 + .../DeviceTypeInterfaces/IDumbSource.cs | 12 + .../DeviceTypeInterfaces/IDvr.cs | 40 + .../DeviceTypeInterfaces/INumeric.cs | 84 + .../DeviceTypeInterfaces/IPower.cs | 48 + .../ISetTopBoxControls.cs | 55 + .../DeviceTypeInterfaces/ITransport.cs | 53 + .../DeviceTypeInterfaces/IUiDisplayInfo.cs | 12 + .../DeviceTypeInterfaces/IWarmingCooling.cs | 17 + .../DeviceTypeInterfaces/Template.cs | 9 + .../Devices/.IHasFeedbacks_BASE_3692.cs.swp | Bin 0 -> 12288 bytes .../Devices/.IHasFeedbacks_LOCAL_3692.cs.swp | Bin 0 -> 12288 bytes .../Devices/.IHasFeedbacks_REMOTE_3692.cs.swp | Bin 0 -> 12288 bytes .../Devices/CodecInterfaces.cs | 46 + .../Devices/CodecInterfaces.cs.orig | 92 + .../Devices/CrestronProcessor.cs | 52 + .../Devices/DeviceApiBase.cs | 17 + .../Devices/DeviceJsonApi.cs | 292 +++ .../Devices/DeviceManager.cs | 268 +++ .../Devices/DeviceManager.cs.orig | 253 ++ .../Devices/DisplayUiConstants.cs | 28 + .../Devices/FIND HOMES Interfaces.cs | 56 + .../Devices/GenericMonitoredTcpDevice.cs | 79 + .../Devices/IAttachVideoStatusExtensions.cs | 39 + .../Devices/IHasFeedbacks.cs | 65 + .../Devices/IHasFeedbacks_BACKUP_3692.cs | 135 ++ .../Devices/IHasFeedbacks_BASE_3692.cs | 63 + .../Devices/IHasFeedbacks_LOCAL_3692.cs | 65 + .../Devices/IHasFeedbacks_REMOTE_3692.cs | 68 + .../Devices/IUsageTracking.cs | 104 + .../Devices/IUsageTracking.cs.orig | 104 + .../Devices/IVolumeAndAudioInterfaces.cs | 106 + .../Devices/IrOutputPortController.cs | 145 ++ .../Devices/NewInterfaces.cs | 56 + .../Devices/PresentationDeviceType.cs | 28 + .../Devices/REMOVE IHasFeedbacks.cs | 56 + .../Devices/ReconfigurableDevice.cs | 63 + .../Devices/SmartObjectBaseTypes.cs | 11 + .../Devices/SourceListItem.cs | 115 + .../Devices/VolumeDeviceChangeEventArgs.cs | 36 + .../Display/BasicIrDisplay.cs | 205 ++ .../Display/DELETE IRDisplayBase.cs | 105 + .../Display/DisplayBase.cs | 127 + .../Display/MockDisplay.cs | 178 ++ .../Ethernet/EthernetStatistics.cs | 44 + .../Factory/DeviceFactory.cs | 66 + .../Feedbacks/BoolFeedback.cs | 116 + .../Feedbacks/BoolFeedbackOneShot.cs | 69 + .../Feedbacks/BoolFeedbackPulseExtender.cs | 78 + .../Feedbacks/BoolOutputLogicals.cs | 131 ++ .../Feedbacks/FeedbackBase.cs | 100 + .../Feedbacks/FeedbackCollection.cs | 25 + .../Feedbacks/FeedbackEventArgs.cs | 48 + .../Feedbacks/IntFeedback.cs | 87 + .../Feedbacks/SerialFeedback.cs | 86 + .../Feedbacks/StringFeedback.cs | 90 + .../Fusion/MOVED FusionSystemController.cs | 377 +++ .../PepperDashEssentialsBase/Global/Global.cs | 60 + .../Global/JobTimer.cs | 80 + .../Global/Scheduler.cs | 139 ++ .../InUseTracking/IInUseTracking.cs | 16 + .../InUseTracking/InUseTracking.cs | 100 + .../License/EssentialsLicenseManager.cs | 98 + .../Lighting/Lighting Interfaces.cs | 59 + .../Lighting/LightingBase.cs | 98 + ...CrestronGenericBaseCommunicationMonitor.cs | 57 + .../Monitoring/GenericCommunicationMonitor.cs | 158 ++ .../Monitoring/Interfaces and things.cs | 57 + .../Monitoring/StatusMonitorBase.cs | 130 ++ .../Monitoring/StatusMonitorCollection.cs | 124 + .../Monitoring/SystemMonitorController.cs | 404 ++++ .../PepperDash_Essentials_Core.csproj | 265 +++ .../Presets/DevicePresets.cs | 178 ++ .../Presets/DevicePresetsView.cs | 60 + .../Presets/Interfaces.cs | 22 + .../Presets/PresetChannel.cs | 28 + .../PresetsListSubpageReferenceListItem.cs | 49 + .../Properties/AssemblyInfo.cs | 7 + .../Properties/ControlSystem.cfg | 0 .../PepperDashEssentialsBase/REMOVE SigId.cs | 37 + .../Ramps and Increments/ActionIncrementer.cs | 120 + .../Ramps and Increments/NumericalHelpers.cs | 40 + .../UshortSigIncrementer.cs | 100 + .../SSMonoIOLibrary.clz/SSMonoIOLibrary.clz | Bin 0 -> 657260 bytes .../SSMonoIOLibrary.config | 16 + .../SSMonoIOLibrary.clz/SimplSharpData.dat | Bin 0 -> 2016 bytes .../SSMonoIOLibrary.clz/manifest.info | 18 + .../SSMonoIOLibrary.clz/manifest.ser | Bin 0 -> 683 bytes .../SSMonoProTaskLibrary.config | 16 + .../SSMonoProTaskLibrary.cplz | Bin 0 -> 195425 bytes .../SimplSharpData.dat | Bin 0 -> 2016 bytes .../SSMonoProTaskLibrary.cplz/manifest.info | 30 + .../SSMonoProTaskLibrary.cplz/manifest.ser | Bin 0 -> 914 bytes .../Room/MOVED RoomEventArgs.cs | 41 + .../PepperDashEssentialsBase/Room/Room.cs | 56 + .../PepperDashEssentialsBase/Room/RoomCues.cs | 61 + .../IRoutingInputsExtensions.cs | 300 +++ .../Routing-CHECK REMOVE/RoutingInterfaces.cs | 57 + .../Routing-CHECK REMOVE/RoutingPort.cs | 144 ++ .../RoutingPortCollection.cs | 26 + .../Routing-CHECK REMOVE/TieLine.cs | 142 ++ .../Routing/DummyRoutingInputsDevice.cs | 36 + .../Routing/ICardPortsDevice.cs | 20 + .../Routing/IRoutingInputsExtensions.cs | 333 +++ .../Routing/RoutingInterfaces.cs | 84 + .../Routing/RoutingPort.cs | 204 ++ .../Routing/RoutingPortCollection.cs | 26 + .../Routing/RoutingPortNames.cs | 203 ++ .../Routing/TieLine.cs | 111 + .../Routing/TieLineConfig.cs | 118 + .../Shades/Shade Interfaces.cs | 119 + .../Shades/ShadeBase.cs | 31 + .../Shades/ShadeController.cs | 58 + .../PepperDashEssentialsBase/SigHelper.cs | 72 + .../SmartObjects/SmartObjectDPad.cs | 24 + .../SmartObjects/SmartObjectDynamicList.cs | 128 + .../SmartObjects/SmartObjectHelper.cs | 128 + .../SmartObjects/SmartObjectHelperBase.cs | 86 + .../SmartObjects/SmartObjectNumeric.cs | 41 + .../SourceListSubpageReferenceList.cs | 167 ++ .../SubpageReferenceList.cs | 263 +++ .../SubpageReferenceListItem.cs | 34 + .../Timers/CountdownTimer.cs | 134 ++ .../CrestronTouchpanelPropertiesConfig.cs | 48 + .../Keyboards/HabaneroKeyboardController.cs | 436 ++++ .../MOVED LargeTouchpanelControllerBase.cs | 275 +++ .../DevicePageControllerBase.cs | 244 ++ .../Touchpanels/ModalDialog.cs | 211 ++ .../Touchpanels/REMOVE Tsr302Controller.cs | 135 ++ .../SmartGraphicsTouchpanelControllerBase.cs | 309 +++ .../Touchpanels/TriListExtensions.cs | 287 +++ .../TriListBridges/HandlerBridge.cs | 26 + .../UI PageManagers/BlurayPageManager.cs | 42 + .../UI PageManagers/PageManager.cs | 96 + .../SetTopBoxThreePanelPageManager.cs | 192 ++ .../SetTopBoxTwoPanelPageManager.cs | 60 + .../UI PageManagers/SinglePageManager.cs | 31 + .../VideoStatus/VideoStatusCues.cs | 20 + .../VideoStatus/VideoStatusOutputs.cs | 148 ++ .../PepperDashEssentialsBase/app.config | 11 + .../Essentials DM/Essentials_DM.sln | 20 + .../AirMedia/AirMediaController.cs | 217 ++ .../AirMedia/AirMediaPropertiesConfig.cs | 21 + .../Cards REMOVE/DmInputCardBase.cs | 62 + .../Cards REMOVE/DmOutputCardBase.cs | 79 + .../Cards REMOVE/Dmc4kCoHdSingleOutputCard.cs | 43 + .../Cards REMOVE/Dmc4kHdoSingleOutputCard.cs | 46 + .../Cards REMOVE/DmcC4kInputCard.cs | 76 + .../Cards REMOVE/DmcHD4kInputCard.cs | 82 + .../Chassis/._DmChassisController.cs | Bin 0 -> 4096 bytes .../Chassis/DmCardAudioOutput.cs | 116 + .../Chassis/DmCardAudioOutput.cs.orig | 120 + .../Chassis/DmCardAudioOutput_BACKUP_14408.cs | 120 + .../Chassis/DmCardAudioOutput_BASE_14408.cs | 111 + .../Chassis/DmCardAudioOutput_LOCAL_14408.cs | 116 + .../Chassis/DmCardAudioOutput_REMOTE_14408.cs | 110 + .../Chassis/DmChassisController.cs | 808 +++++++ .../Chassis/HdMdNxM4kEController.cs | 125 + .../Essentials_DM/Config/DMChassisConfig.cs | 47 + .../Essentials_DM/Config/DeviceFactory.cs | 79 + .../Essentials_DM/Config/DmRmcConfig.cs | 25 + .../Essentials_DM/Config/DmTxConfig.cs | 28 + .../Config/HdMdNxM4kEPropertiesConfig.cs | 23 + .../Config/InputPropertiesConfig.cs | 15 + .../Essentials DM/Essentials_DM/DmPortName.cs | 37 + .../Endpoints/DGEs/DgeController.cs | 64 + .../Endpoints/DGEs/DgePropertiesConfig.cs | 18 + .../Endpoints/NVX/DmNvx35xController.cs | 19 + .../Endpoints/NVX/DmNvxConfig.cs | 30 + .../Endpoints/NVX/DmNvxControllerBase.cs | 30 + .../Receivers/DmHdBaseTEndpointController.cs | 49 + .../Receivers/DmRmc100SController.cs | 73 + .../Receivers/DmRmc150SController.cs | 112 + .../Receivers/DmRmc200CController.cs | 112 + .../Receivers/DmRmc200S2Controller.cs | 112 + .../Receivers/DmRmc200SController.cs | 112 + .../Receivers/DmRmc4KScalerCController.cs | 198 ++ .../Receivers/DmRmc4k100C1GController.cs | 59 + .../Receivers/DmRmc4kScalerCDspController.cs | 198 ++ .../Endpoints/Receivers/DmRmcHelper.cs | 250 ++ .../Receivers/DmRmcScalerCController.cs | 115 + .../Receivers/DmRmcScalerS2Controller.cs | 115 + .../Receivers/DmRmcScalerSController.cs | 115 + .../Receivers/DmRmcX100CController.cs | 73 + .../Transmitters/DmTx200Controller.cs | 308 +++ .../Transmitters/DmTx201CController.cs | 312 +++ .../Transmitters/DmTx401CController.cs | 323 +++ .../Transmitters/DmTx4k100Controller.cs | 105 + .../Transmitters/DmTx4k202CController.cs | 348 +++ .../Transmitters/DmTx4k302CController.cs | 369 +++ .../Transmitters/DmTx4kz302CController.cs | 340 +++ .../Endpoints/Transmitters/DmTxHelpers.cs | 181 ++ .../Essentials_DM/Essentials_DM.csproj | 160 ++ .../Essentials DM/Essentials_DM/Extensions.cs | 43 + .../Essentials_DM/IDmHdmiInputExtensions.cs | 60 + .../MOVE IBasicVideoStatusFeedbacks.cs | 19 + .../Nvx/NvxDirectorController.cs | 746 ++++++ .../Essentials_DM/Properties/AssemblyInfo.cs | 7 + .../Properties/ControlSystem.cfg | 0 .../Essentials_DM/VideoStatusHelpers.cs | 73 + .../Essentials DM/Essentials_DM/app.config | 11 + .../Essentials Devices Common.sln | 20 + .../Audio/GenericAudioOut.cs | 75 + .../AudioCodec/AudioCodecBase.cs | 104 + .../AudioCodec/Interfaces/IAudioCodecInfo.cs | 24 + .../AudioCodec/Interfaces/IHasAudioCodec.cs | 24 + .../AudioCodec/MockAC/MockAC.cs | 114 + .../MockAC/MockAcPropertiesConfig.cs | 16 + .../Cameras/CameraBase.cs | 72 + .../Cameras/CameraVisca.cs | 210 ++ .../ClassDiagram1.cd | 740 ++++++ .../Codec/CodecActiveCallItem.cs | 67 + .../Codec/CodecActiveCallItem.cs.orig | 50 + .../Codec/eCodecCallDirection.cs | 41 + .../Codec/eCodecCallStatus.cs | 89 + .../Codec/eCodecCallType.cs | 54 + .../Codec/eMeetingPrivacy.cs | 42 + .../Codec/iCodecAudio.cs | 18 + .../Codec/iHasCallFavorites.cs | 26 + .../Codec/iHasCallHistory.cs | 141 ++ .../Codec/iHasContentSharing.cs | 24 + .../Codec/iHasDialer.cs | 30 + .../Codec/iHasDirectory.cs | 230 ++ .../Codec/iHasScheduleAwareness.cs | 166 ++ .../Crestron/Gateways/CenRfgwController.cs | 115 + .../DSP/BiampTesira/BiampTesiraForteDsp.cs | 376 +++ .../BiampTesira/BiampTesiraForteDspLevel.cs | 366 +++ .../BiampTesiraFortePropertiesConfig.cs | 40 + .../BiampTesira/TesiraForteControlPoint.cs | 115 + .../DSP/BiampTesira/TesiraForteMuteControl.cs | 79 + .../Essentials Devices Common/DSP/DspBase.cs | 75 + .../SoundStructureBasics.cs | 21 + .../DSP/QSC/QscDsp.cs | 328 +++ .../DSP/QSC/QscDspControlPoint.cs | 65 + .../DSP/QSC/QscDspDialer.cs | 270 +++ .../DSP/QSC/QscDspLevelControl.cs | 307 +++ .../DSP/QSC/QscDspPropertiesConfig.cs | 88 + .../DSP/QscDsp/._QscDspPropertiesConfig.cs | Bin 0 -> 4096 bytes .../DSP/QscDsp/QscDsp.cs | 328 +++ .../DSP/QscDsp/QscDspControlPoint.cs | 65 + .../DSP/QscDsp/QscDspDialer.cs | 270 +++ .../DSP/QscDsp/QscDspLevelControl.cs | 307 +++ .../DSP/QscDsp/QscDspPropertiesConfig.cs | 88 + .../DiscPlayer/IRDiscPlayerBase.cs | 314 +++ .../Display/AvocorVTFDisplay.cs | 720 ++++++ .../Display/ComTcpDisplayBase.cs | 43 + .../Display/DeviceFactory.cs | 64 + .../Display/InputInterfaces.cs | 17 + .../Display/NECPSXMDisplay.cs | 356 +++ .../Display/NecPaSeriesProjector.cs | 243 ++ .../Display/PanasonicThDisplay.cs | 338 +++ .../Display/SamsungMDCDisplay.cs | 620 +++++ .../Environment/Crestron Lighting/Din8sw8.cs | 88 + .../Environment/Lutron/LutronQuantum.cs | 256 ++ .../Environment/Somfy/RelayControlledShade.cs | 113 + .../Essentials Devices Common.csproj | 205 ++ .../Evertz/EvertsEndpointStatusServer.cs | 152 ++ .../Evertz/EvertzEndpoint.cs | 337 +++ .../Evertz/EvertzEndpointPropertiesConfig.cs | 26 + .../Evertz/EvertzEndpointVarIds.cs | 13 + .../Evertz/GenericHttpClient.cs | 122 + .../Factory/DeviceFactory.cs | 407 ++++ .../Generic/GenericSource.cs | 42 + .../AnalogWayLiveCorePropertiesConfig.cs | 24 + .../AnalogWay/AnalongWayLiveCore.cs | 241 ++ .../ImageProcessors/TVOne/TVOneCorio.cs | 241 ++ .../TVOne/TVOneCorioPropertiesConfig.cs | 24 + .../ImageProcessors/TVOneCorio.cs | 241 ++ .../TVOneCorioPropertiesConfig.cs | 24 + .../MIcrophone/MicrophonePrivacyController.cs | 226 ++ .../MicrophonePrivacyControllerConfig.cs | 22 + ...entialsGlsOccupancySensorBaseController.cs | 73 + ...lsGlsOccupancySensorBaseController.cs.orig | 87 + .../EssentialsOccupancyAggregator.cs | 45 + .../Occupancy/iOccupancyStatusProvider.cs | 15 + .../Essentials Devices Common/PC/InRoomPc.cs | 66 + .../Essentials Devices Common/PC/Laptop.cs | 66 + .../DigitalLoggerPropertiesConfig.cs | 25 + .../Power Controllers/Digitallogger.cs | 313 +++ .../Properties/AssemblyInfo.cs | 7 + .../Properties/ControlSystem.cfg | 0 .../SetTopBox/IRSetTopBoxBase.cs | 343 +++ .../SetTopBox/SetTopBoxPropertiesConfig.cs | 20 + .../Streaming/AppleTV.cs | 145 ++ .../Streaming/Roku.cs | 148 ++ .../CiscoCodec/BookingsDataClasses.cs | 379 +++ .../CiscoCodec/CallHistoryDataClasses.cs | 97 + .../VideoCodec/CiscoCodec/CiscoCamera.cs | 312 +++ .../VideoCodec/CiscoCodec/CiscoCodec.cs.orig | 1359 +++++++++++ .../VideoCodec/CiscoCodec/CiscoSparkCodec.cs | 1846 +++++++++++++++ .../CiscoCodec/CiscoSparkCodec.cs.orig | 1487 ++++++++++++ .../CiscoSparkCodecPropertiesConfig.cs | 33 + .../VideoCodec/CiscoCodec/HttpApiServer.cs | 106 + .../CiscoCodec/PhonebookDataClasses.cs | 397 ++++ .../VideoCodec/CiscoCodec/RoomPresets.cs | 100 + .../VideoCodec/CiscoCodec/xConfiguration.cs | 1807 ++++++++++++++ .../VideoCodec/CiscoCodec/xEvent.cs | 141 ++ .../VideoCodec/CiscoCodec/xStatus.cs | 2066 +++++++++++++++++ .../VideoCodec/CiscoCodec/xStatusSparkPlus.cs | 1552 +++++++++++++ .../VideoCodec/CodecActiveCallItem.cs.orig | 43 + .../VideoCodec/Interfaces/CameraControl.cs | 139 ++ .../VideoCodec/Interfaces/IHasCodecLayouts.cs | 22 + .../Interfaces/IHasCodecSelfview.cs | 26 + .../VideoCodec/Interfaces/IHasVideoCodec.cs | 40 + .../VideoCodec/Interfaces/iVideoCodecInfo.cs | 30 + .../VideoCodec/MockVC/MockCodecDirectory.cs | 419 ++++ .../VideoCodec/MockVC/MockVC.cs | 773 ++++++ .../VideoCodec/MockVC/MockVC.cs.orig | 250 ++ .../VideoCodec/MockVC/MockVCCamera.cs | 195 ++ .../MockVC/MockVcPropertiesConfig.cs | 22 + .../VideoCodec/VideoCodecBase.cs | 328 +++ .../VideoCodec/VideoCodecBase.cs.orig | 152 ++ .../VideoCodec/ZoomRoom/ResponseObjects.cs | 1184 ++++++++++ .../VideoCodec/ZoomRoom/ZoomRoom.cs | 1679 ++++++++++++++ .../VideoCodec/ZoomRoom/ZoomRoomCamera.cs | 213 ++ .../ZoomRoom/ZoomRoomPropertiesConfig.cs | 16 + essentials-framework/Essentials Framework.sln | 32 + .../Bridges/Bridges.BridgeFactory.cs | 69 + .../Bridges/EssentialComms.cs | 138 ++ .../Bridges/EssentialDM.cs | 120 + .../Factories/DmFactory.cs | 96 + .../PepperDashEssentials/ControlSystem.cs | 309 +++ .../PepperDashEssentials.csproj | 224 ++ .../UIDrivers/DualDisplayRouting REMOVE.cs | 231 ++ .../references/PepperDash_Core.dll | Bin 0 -> 142520 bytes 362 files changed, 54624 insertions(+), 5 deletions(-) delete mode 160000 essentials-framework create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase.sln create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CecPortController.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ComPortController.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ComSpecJsonConverter.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CommFactory.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CommunicationExtras.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ConsoleCommMockDevice.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/DELETE ComPortController.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/GenericComm.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/IRPortHelper.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/BasicConfig.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/ConfigPropertiesHelpers.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/DeviceConfig.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigReader.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigUpdater.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigWriter.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/EssentialsConfig.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/InfoConfig.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/SourceDevicePropertiesConfigBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Constants/CommonCues.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/IOPortConfig.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/GenericDigitalInputDevice.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/GenericVersiportInputDevice.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/IDigitalInput.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Relay/GenericRelayDevice.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Relay/ISwitchedOutput.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron/CrestronGenericBaseDevice.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron/CrestronGenericBaseDevice.cs.orig create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Cues and DevAction/Cues.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceControlsParentInterfaces/IPresentationSource.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IChannel.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IColorFunctions.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDPad.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDiscPlayerControls.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDisplayBasic.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDumbSource.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDvr.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/INumeric.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IPower.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/ISetTopBoxControls.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/ITransport.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IUiDisplayInfo.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IWarmingCooling.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/Template.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/.IHasFeedbacks_BASE_3692.cs.swp create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/.IHasFeedbacks_LOCAL_3692.cs.swp create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/.IHasFeedbacks_REMOTE_3692.cs.swp create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CodecInterfaces.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CodecInterfaces.cs.orig create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CrestronProcessor.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceApiBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceJsonApi.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceManager.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceManager.cs.orig create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DisplayUiConstants.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/FIND HOMES Interfaces.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/GenericMonitoredTcpDevice.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IAttachVideoStatusExtensions.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_BACKUP_3692.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_BASE_3692.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_LOCAL_3692.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_REMOTE_3692.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IUsageTracking.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IUsageTracking.cs.orig create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IVolumeAndAudioInterfaces.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IrOutputPortController.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/NewInterfaces.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/PresentationDeviceType.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/REMOVE IHasFeedbacks.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/ReconfigurableDevice.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/SmartObjectBaseTypes.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/SourceListItem.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/VolumeDeviceChangeEventArgs.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/BasicIrDisplay.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/DELETE IRDisplayBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/DisplayBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/MockDisplay.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Ethernet/EthernetStatistics.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Factory/DeviceFactory.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedback.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedbackOneShot.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedbackPulseExtender.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolOutputLogicals.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackCollection.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackEventArgs.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/IntFeedback.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/SerialFeedback.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/StringFeedback.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Fusion/MOVED FusionSystemController.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/Global.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/JobTimer.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/Scheduler.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/InUseTracking/IInUseTracking.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/InUseTracking/InUseTracking.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/License/EssentialsLicenseManager.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Lighting/Lighting Interfaces.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Lighting/LightingBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/CrestronGenericBaseCommunicationMonitor.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/GenericCommunicationMonitor.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/Interfaces and things.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/StatusMonitorBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/StatusMonitorCollection.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/SystemMonitorController.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/PepperDash_Essentials_Core.csproj create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/DevicePresets.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/DevicePresetsView.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/Interfaces.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/PresetChannel.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/PresetsListSubpageReferenceListItem.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Properties/AssemblyInfo.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Properties/ControlSystem.cfg create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/REMOVE SigId.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/ActionIncrementer.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/NumericalHelpers.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/UshortSigIncrementer.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/SSMonoIOLibrary.clz create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/SSMonoIOLibrary.config create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/SimplSharpData.dat create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/manifest.info create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/manifest.ser create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/SSMonoProTaskLibrary.config create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/SSMonoProTaskLibrary.cplz create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/SimplSharpData.dat create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/manifest.info create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/manifest.ser create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/MOVED RoomEventArgs.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Room.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/RoomCues.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/IRoutingInputsExtensions.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingInterfaces.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingPort.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingPortCollection.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/TieLine.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/DummyRoutingInputsDevice.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/ICardPortsDevice.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/IRoutingInputsExtensions.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingInterfaces.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPort.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortCollection.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortNames.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/TieLine.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/TieLineConfig.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/Shade Interfaces.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/ShadeBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/ShadeController.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/SigHelper.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectDPad.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectDynamicList.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectHelper.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectHelperBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectNumeric.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SourceListSubpageReferenceList.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SubpageReferenceList.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SubpageReferenceListItem.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Timers/CountdownTimer.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/CrestronTouchpanelPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/Keyboards/HabaneroKeyboardController.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/MOVED LargeTouchpanelControllerBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/MOVED UIControllers/DevicePageControllerBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/ModalDialog.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/REMOVE Tsr302Controller.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/SmartGraphicsTouchpanelControllerBase.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/TriListExtensions.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/TriListBridges/HandlerBridge.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/BlurayPageManager.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/PageManager.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SetTopBoxThreePanelPageManager.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SetTopBoxTwoPanelPageManager.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SinglePageManager.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/VideoStatus/VideoStatusCues.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/VideoStatus/VideoStatusOutputs.cs create mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/app.config create mode 100644 essentials-framework/Essentials DM/Essentials_DM.sln create mode 100644 essentials-framework/Essentials DM/Essentials_DM/AirMedia/AirMediaController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/AirMedia/AirMediaPropertiesConfig.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmInputCardBase.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmOutputCardBase.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/Dmc4kCoHdSingleOutputCard.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/Dmc4kHdoSingleOutputCard.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmcC4kInputCard.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmcHD4kInputCard.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/._DmChassisController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput.cs.orig create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_BACKUP_14408.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_BASE_14408.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_LOCAL_14408.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_REMOTE_14408.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/DmChassisController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/HdMdNxM4kEController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Config/DMChassisConfig.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Config/DeviceFactory.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Config/DmRmcConfig.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Config/DmTxConfig.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Config/HdMdNxM4kEPropertiesConfig.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Config/InputPropertiesConfig.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/DmPortName.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/DGEs/DgeController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/DGEs/DgePropertiesConfig.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvx35xController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvxConfig.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvxControllerBase.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmHdBaseTEndpointController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc100SController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc150SController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200CController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200S2Controller.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200SController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4KScalerCController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4k100C1GController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4kScalerCDspController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcHelper.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerCController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerS2Controller.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerSController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcX100CController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx200Controller.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx201CController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx401CController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k100Controller.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k202CController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k302CController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4kz302CController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTxHelpers.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Essentials_DM.csproj create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Extensions.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/IDmHdmiInputExtensions.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/MOVE IBasicVideoStatusFeedbacks.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Nvx/NvxDirectorController.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Properties/AssemblyInfo.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/Properties/ControlSystem.cfg create mode 100644 essentials-framework/Essentials DM/Essentials_DM/VideoStatusHelpers.cs create mode 100644 essentials-framework/Essentials DM/Essentials_DM/app.config create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common.sln create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Audio/GenericAudioOut.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/AudioCodecBase.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/Interfaces/IAudioCodecInfo.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/Interfaces/IHasAudioCodec.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/MockAC/MockAC.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/MockAC/MockAcPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Cameras/CameraBase.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Cameras/CameraVisca.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/ClassDiagram1.cd create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/CodecActiveCallItem.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/CodecActiveCallItem.cs.orig create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallDirection.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallStatus.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallType.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eMeetingPrivacy.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iCodecAudio.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasCallFavorites.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasCallHistory.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasContentSharing.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasDialer.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasDirectory.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasScheduleAwareness.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Crestron/Gateways/CenRfgwController.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraForteDsp.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraForteDspLevel.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraFortePropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/TesiraForteControlPoint.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/TesiraForteMuteControl.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/DspBase.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/PolycomSoundStructure/SoundStructureBasics.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDsp.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspControlPoint.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspDialer.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspLevelControl.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/._QscDspPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDsp.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspControlPoint.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspDialer.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspLevelControl.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/DiscPlayer/IRDiscPlayerBase.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/AvocorVTFDisplay.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/ComTcpDisplayBase.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/DeviceFactory.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/InputInterfaces.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/NECPSXMDisplay.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/NecPaSeriesProjector.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/PanasonicThDisplay.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/SamsungMDCDisplay.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Crestron Lighting/Din8sw8.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Lutron/LutronQuantum.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Somfy/RelayControlledShade.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Essentials Devices Common.csproj create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertsEndpointStatusServer.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpoint.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpointPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpointVarIds.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/GenericHttpClient.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Factory/DeviceFactory.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Generic/GenericSource.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/AnalogWay/AnalogWayLiveCorePropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/AnalogWay/AnalongWayLiveCore.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOne/TVOneCorio.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOne/TVOneCorioPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOneCorio.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOneCorioPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/MIcrophone/MicrophonePrivacyController.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/MIcrophone/MicrophonePrivacyControllerConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsGlsOccupancySensorBaseController.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsGlsOccupancySensorBaseController.cs.orig create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsOccupancyAggregator.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/iOccupancyStatusProvider.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/PC/InRoomPc.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/PC/Laptop.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Power Controllers/DigitalLoggerPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Power Controllers/Digitallogger.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Properties/AssemblyInfo.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Properties/ControlSystem.cfg create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/SetTopBox/IRSetTopBoxBase.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/SetTopBox/SetTopBoxPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Streaming/AppleTV.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/Streaming/Roku.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/BookingsDataClasses.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CallHistoryDataClasses.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoCamera.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoCodec.cs.orig create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodec.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodec.cs.orig create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodecPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/HttpApiServer.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/PhonebookDataClasses.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/RoomPresets.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xConfiguration.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xEvent.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xStatus.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xStatusSparkPlus.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CodecActiveCallItem.cs.orig create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/CameraControl.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasCodecLayouts.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasCodecSelfview.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasVideoCodec.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/iVideoCodecInfo.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockCodecDirectory.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVC.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVC.cs.orig create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVCCamera.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVcPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs.orig create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ResponseObjects.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoom.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomCamera.cs create mode 100644 essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomPropertiesConfig.cs create mode 100644 essentials-framework/Essentials Framework.sln create mode 100644 essentials-framework/Essentials/PepperDashEssentials/Bridges/Bridges.BridgeFactory.cs create mode 100644 essentials-framework/Essentials/PepperDashEssentials/Bridges/EssentialComms.cs create mode 100644 essentials-framework/Essentials/PepperDashEssentials/Bridges/EssentialDM.cs create mode 100644 essentials-framework/Essentials/PepperDashEssentials/Configuration Original/Factories/DmFactory.cs create mode 100644 essentials-framework/Essentials/PepperDashEssentials/ControlSystem.cs create mode 100644 essentials-framework/Essentials/PepperDashEssentials/PepperDashEssentials.csproj create mode 100644 essentials-framework/Essentials/PepperDashEssentials/UIDrivers/DualDisplayRouting REMOVE.cs create mode 100644 essentials-framework/references/PepperDash_Core.dll diff --git a/.gitignore b/.gitignore index 6c7dbb9d..4d389dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ obj/ [Rr]elease*/ _ReSharper*/ SIMPLSharpLogs/ -*.projectinfo \ No newline at end of file +*.projectinfo +essentials-framework/EssentialDMTestConfig/ diff --git a/.gitmodules b/.gitmodules index 2ae0902f..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "essentials-framework"] - path = essentials-framework - url = https://bitbucket.org/Pepperdash_Products/essentials-framework.git diff --git a/essentials-framework b/essentials-framework deleted file mode 160000 index 2e132f83..00000000 --- a/essentials-framework +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2e132f830f947c62ca148d7ce8569b6a3d604cab diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase.sln b/essentials-framework/Essentials Core/PepperDashEssentialsBase.sln new file mode 100644 index 00000000..a51c8c5d --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash_Essentials_Core", "PepperDashEssentialsBase\PepperDash_Essentials_Core.csproj", "{A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CecPortController.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CecPortController.cs new file mode 100644 index 00000000..a2493e3c --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CecPortController.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public class CecPortController : Device, IBasicCommunication + { + public event EventHandler BytesReceived; + public event EventHandler TextReceived; + + public bool IsConnected { get { return true; } } + + ICec Port; + + public CecPortController(string key, ICec port) + : base(key) + { + Port = port; + + Port.StreamCec.CecChange += new CecChangeEventHandler(StreamCec_CecChange); + } + + void StreamCec_CecChange(Cec cecDevice, CecEventArgs args) + { + if (args.EventId == CecEventIds.CecMessageReceivedEventId) + OnDataReceived(cecDevice.Received.StringValue); + else if (args.EventId == CecEventIds.ErrorFeedbackEventId) + if(cecDevice.ErrorFeedback.BoolValue) + Debug.Console(2, this, "CEC NAK Error"); + } + + void OnDataReceived(string s) + { + var bytesHandler = BytesReceived; + if (bytesHandler != null) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(s); + bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + } + var textHandler = TextReceived; + if (textHandler != null) + textHandler(this, new GenericCommMethodReceiveTextArgs(s)); + } + + #region IBasicCommunication Members + + public void SendText(string text) + { + if (Port == null) + return; + Port.StreamCec.Send.StringValue = text; + } + + public void SendBytes(byte[] bytes) + { + if (Port == null) + return; + var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + Port.StreamCec.Send.StringValue = text; + } + + public void Connect() + { + } + + public void Disconnect() + { + } + + #endregion + + /// + /// + /// + /// + public void SimulateReceive(string s) + { + // split out hex chars and build string + var split = Regex.Split(s, @"(\\[Xx][0-9a-fA-F][0-9a-fA-F])"); + StringBuilder b = new StringBuilder(); + foreach (var t in split) + { + if (t.StartsWith(@"\") && t.Length == 4) + b.Append((char)(Convert.ToByte(t.Substring(2, 2), 16))); + else + b.Append(t); + } + + OnDataReceived(b.ToString()); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ComPortController.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ComPortController.cs new file mode 100644 index 00000000..a5a5b14f --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ComPortController.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + public class ComPortController : Device, IBasicCommunication + { + public event EventHandler BytesReceived; + public event EventHandler TextReceived; + + public bool IsConnected { get { return true; } } + + ComPort Port; + ComPort.ComPortSpec Spec; + + public ComPortController(string key, ComPort port, ComPort.ComPortSpec spec) + : base(key) + { + if (port == null) + { + Debug.Console(0, this, "ERROR: Invalid com port, continuing but comms will not function"); + return; + } + + Port = port; + Spec = spec; + //IsConnected = new BoolFeedback(CommonBoolCue.IsConnected, () => true); + + if (Port.Parent is CrestronControlSystem) + { + + + var result = Port.Register(); + if (result != eDeviceRegistrationUnRegistrationResponse.Success) + { + Debug.Console(0, this, "ERROR: Cannot register Com port: {0}", result); + return; // false + } + } + var specResult = Port.SetComPortSpec(Spec); + if (specResult != 0) + { + Debug.Console(0, this, "WARNING: Cannot set comspec"); + return; // false + } + Port.SerialDataReceived += new ComPortDataReceivedEvent(Port_SerialDataReceived); + } + + ~ComPortController() + { + Port.SerialDataReceived -= Port_SerialDataReceived; + } + + void Port_SerialDataReceived(ComPort ReceivingComPort, ComPortSerialDataEventArgs args) + { + OnDataReceived(args.SerialData); + } + + void OnDataReceived(string s) + { + var bytesHandler = BytesReceived; + if (bytesHandler != null) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(s); + bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + } + var textHandler = TextReceived; + if (textHandler != null) + textHandler(this, new GenericCommMethodReceiveTextArgs(s)); + } + + public override bool Deactivate() + { + return Port.UnRegister() == eDeviceRegistrationUnRegistrationResponse.Success; + } + + #region IBasicCommunication Members + + public void SendText(string text) + { + if (Port == null) + return; + Port.Send(text); + } + + public void SendBytes(byte[] bytes) + { + if (Port == null) + return; + var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + Port.Send(text); + } + + public void Connect() + { + } + + public void Disconnect() + { + } + + #endregion + + /// + /// + /// + /// + public void SimulateReceive(string s) + { + // split out hex chars and build string + var split = Regex.Split(s, @"(\\[Xx][0-9a-fA-F][0-9a-fA-F])"); + StringBuilder b = new StringBuilder(); + foreach (var t in split) + { + if (t.StartsWith(@"\") && t.Length == 4) + b.Append((char)(Convert.ToByte(t.Substring(2, 2), 16))); + else + b.Append(t); + } + + OnDataReceived(b.ToString()); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ComSpecJsonConverter.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ComSpecJsonConverter.cs new file mode 100644 index 00000000..fe77bf83 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ComSpecJsonConverter.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// This converter creates a proper ComPort.ComPortSpec struct from more-friendly JSON values. It uses + /// ComSpecPropsJsonConverter to finish the individual properties. + /// + public class ComSpecJsonConverter : JsonConverter + { + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (objectType == typeof(ComPort.ComPortSpec)) + { + var newSer = new JsonSerializer(); + newSer.Converters.Add(new ComSpecPropsJsonConverter()); + newSer.ObjectCreationHandling = ObjectCreationHandling.Replace; + return newSer.Deserialize(reader); + } + return null; + } + + /// + /// + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ComPort.ComPortSpec); + } + + public override bool CanRead { get { return true; } } + + /// + /// This converter will not be used for writing + /// + public override bool CanWrite { get { return false; } } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } + + /// + /// The gist of this converter: The comspec JSON comes in with normal values that need to be converted + /// into enum names. This converter takes the value and applies the appropriate enum's name prefix to the value + /// and then returns the enum value using Enum.Parse. NOTE: Does not write + /// + public class ComSpecPropsJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ComPort.eComBaudRates) + || objectType == typeof(ComPort.eComDataBits) + || objectType == typeof(ComPort.eComParityType) + || objectType == typeof(ComPort.eComHardwareHandshakeType) + || objectType == typeof(ComPort.eComSoftwareHandshakeType) + || objectType == typeof(ComPort.eComProtocolType) + || objectType == typeof(ComPort.eComStopBits); + } + + public override bool CanRead { get { return true; } } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + //Debug.Console(2, "ReadJson type: " + objectType.Name); + if (objectType == typeof(ComPort.eComBaudRates)) + return Enum.Parse(typeof(ComPort.eComBaudRates), "ComspecBaudRate" + reader.Value, false); + else if (objectType == typeof(ComPort.eComDataBits)) + return Enum.Parse(typeof(ComPort.eComDataBits), "ComspecDataBits" + reader.Value, true); + else if (objectType == typeof(ComPort.eComHardwareHandshakeType)) + return Enum.Parse(typeof(ComPort.eComHardwareHandshakeType), "ComspecHardwareHandshake" + reader.Value, true); + else if (objectType == typeof(ComPort.eComParityType)) + return Enum.Parse(typeof(ComPort.eComParityType), "ComspecParity" + reader.Value, true); + else if (objectType == typeof(ComPort.eComProtocolType)) + return Enum.Parse(typeof(ComPort.eComProtocolType), "ComspecProtocol" + reader.Value, true); + else if (objectType == typeof(ComPort.eComSoftwareHandshakeType)) + return Enum.Parse(typeof(ComPort.eComSoftwareHandshakeType), "ComspecSoftwareHandshake" + reader.Value, true); + else if (objectType == typeof(ComPort.eComStopBits)) + return Enum.Parse(typeof(ComPort.eComStopBits), "ComspecStopBits" + reader.Value, true); + return null; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } + + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CommFactory.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CommFactory.cs new file mode 100644 index 00000000..23aea272 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CommFactory.cs @@ -0,0 +1,196 @@ +using System; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public class CommFactory + { + public static EssentialsControlPropertiesConfig GetControlPropertiesConfig(DeviceConfig deviceConfig) + { + try + { + return JsonConvert.DeserializeObject + (deviceConfig.Properties["control"].ToString()); + //Debug.Console(2, "Control TEST: {0}", JsonConvert.SerializeObject(controlConfig)); + } + catch (Exception e) + { + + Debug.Console(0, "ERROR: [{0}] Control properties deserialize failed:\r{1}", deviceConfig.Key, e); + return null; + } + } + + + /// + /// Returns a comm method of either com port, TCP, SSH, and puts this into the DeviceManager + /// + /// The Device config object + public static IBasicCommunication CreateCommForDevice(DeviceConfig deviceConfig) + { + EssentialsControlPropertiesConfig controlConfig = GetControlPropertiesConfig(deviceConfig); + if (controlConfig == null) + return null; + + IBasicCommunication comm = null; + try + { + var c = controlConfig.TcpSshProperties; + switch (controlConfig.Method) + { + case eControlMethod.Com: + comm = new ComPortController(deviceConfig.Key + "-com", GetComPort(controlConfig), controlConfig.ComParams); + break; + case eControlMethod.Cec: + comm = new CecPortController(deviceConfig.Key + "-cec", GetCecPort(controlConfig)); + break; + case eControlMethod.IR: + break; + case eControlMethod.Ssh: + { + var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password); + ssh.AutoReconnect = c.AutoReconnect; + if(ssh.AutoReconnect) + ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; + comm = ssh; + break; + } + case eControlMethod.Tcpip: + { + var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize); + tcp.AutoReconnect = c.AutoReconnect; + if (tcp.AutoReconnect) + tcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; + comm = tcp; + break; + } + case eControlMethod.Udp: + { + var udp = new GenericUdpServer(deviceConfig.Key + "-udp", c.Address, c.Port, c.BufferSize); + comm = udp; + break; + } + case eControlMethod.Telnet: + break; + default: + break; + } + } + catch (Exception e) + { + Debug.Console(0, "Cannot create communication from JSON:\r{0}\r\rException:\r{1}", + deviceConfig.Properties.ToString(), e); + } + + // put it in the device manager if it's the right flavor + var comDev = comm as Device; + if (comDev != null) + DeviceManager.AddDevice(comDev); + return comm; + } + + public static ComPort GetComPort(EssentialsControlPropertiesConfig config) + { + var comPar = config.ComParams; + var dev = GetIComPortsDeviceFromManagedDevice(config.ControlPortDevKey); + if (dev != null && config.ControlPortNumber <= dev.NumberOfComPorts) + return dev.ComPorts[config.ControlPortNumber]; + Debug.Console(0, "GetComPort: Device '{0}' does not have com port {1}", config.ControlPortDevKey, config.ControlPortNumber); + return null; + } + + /// + /// Gets an ICec port from a RoutingInput or RoutingOutput on a device + /// + /// + /// + public static ICec GetCecPort(ControlPropertiesConfig config) + { + var dev = DeviceManager.GetDeviceForKey(config.ControlPortDevKey); + + if (dev != null) + { + var inputPort = (dev as IRoutingInputsOutputs).InputPorts[config.ControlPortName]; + + if (inputPort != null) + if (inputPort.Port is ICec) + return inputPort.Port as ICec; + + var outputPort = (dev as IRoutingInputsOutputs).OutputPorts[config.ControlPortName]; + + if (outputPort != null) + if (outputPort.Port is ICec) + return outputPort.Port as ICec; + } + Debug.Console(0, "GetCecPort: Device '{0}' does not have a CEC port called: '{1}'", config.ControlPortDevKey, config.ControlPortName); + return null; + } + + /// + /// Helper to grab the IComPorts device for this PortDeviceKey. Key "controlSystem" will + /// return the ControlSystem object from the Global class. + /// + /// IComPorts device or null if the device is not found or does not implement IComPorts + public static IComPorts GetIComPortsDeviceFromManagedDevice(string ComPortDevKey) + { + if ((ComPortDevKey.Equals("controlSystem", System.StringComparison.OrdinalIgnoreCase) + || ComPortDevKey.Equals("processor", System.StringComparison.OrdinalIgnoreCase)) + && Global.ControlSystem is IComPorts) + return Global.ControlSystem; + else + { + var dev = DeviceManager.GetDeviceForKey(ComPortDevKey) as IComPorts; + if (dev == null) + Debug.Console(0, "ComPortConfig: Cannot find com port device '{0}'", ComPortDevKey); + return dev; + } + } + } + + /// + /// + /// + public class EssentialsControlPropertiesConfig : + PepperDash.Core.ControlPropertiesConfig + { + + [JsonConverter(typeof(ComSpecJsonConverter))] + public ComPort.ComPortSpec ComParams { get; set; } + + public string CresnetId { get; set; } + + /// + /// Attempts to provide uint conversion of string CresnetId + /// + public uint CresnetIdInt + { + get + { + try + { + return Convert.ToUInt32(CresnetId, 16); + } + catch (Exception) + { + throw new FormatException(string.Format("ERROR:Unable to convert Cresnet ID: {0} to hex. Error:\n{1}", CresnetId)); + } + } + } + } + + public class IrControlSpec + { + public string PortDeviceKey { get; set; } + public uint PortNumber { get; set; } + public string File { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CommunicationExtras.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CommunicationExtras.cs new file mode 100644 index 00000000..05fb3954 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/CommunicationExtras.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public interface IComPortsDevice + { + IComPorts Device { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ConsoleCommMockDevice.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ConsoleCommMockDevice.cs new file mode 100644 index 00000000..d833aafa --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/ConsoleCommMockDevice.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public class ConsoleCommMockDevice : Device, ICommunicationMonitor + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + + /// + /// Defaults to \x0a + /// + public string LineEnding { get; set; } + + /// + /// Set to true to show responses in full hex + /// + public bool ShowHexResponse { get; set; } + + public ConsoleCommMockDevice(string key, string name, ConsoleCommMockDevicePropertiesConfig props, IBasicCommunication comm) + :base(key, name) + { + Communication = comm; + PortGather = new CommunicationGather(Communication, '\x0d'); + PortGather.LineReceived += this.Port_LineReceived; + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + LineEnding = props.LineEnding; + } + + public override bool CustomActivate() + { + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + CrestronConsole.AddNewConsoleCommand(SendLine, "send" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + return true; + } + + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + if (Debug.Level == 2) + Debug.Console(2, this, "RX: '{0}'", + ShowHexResponse ? ComTextHelper.GetEscapedText(args.Text) : args.Text); + } + + void SendLine(string s) + { + //if (Debug.Level == 2) + // Debug.Console(2, this, " Send '{0}'", ComTextHelper.GetEscapedText(s)); + Communication.SendText(s + LineEnding); + } + } + + public class ConsoleCommMockDevicePropertiesConfig + { + public string LineEnding { get; set; } + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public ConsoleCommMockDevicePropertiesConfig() + { + LineEnding = "\x0a"; + } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/DELETE ComPortController.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/DELETE ComPortController.cs new file mode 100644 index 00000000..ece72b20 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/DELETE ComPortController.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using Newtonsoft.Json; + +namespace PepperDash.Essentials.Core +{ + public class ComPortController : Device, IBasicCommunication + { + public event EventHandler BytesReceived; + public event EventHandler TextReceived; + + ComPort Port; + ComPort.ComPortSpec Spec; + + public ComPortController(string key, IComPorts ComDevice, uint comPortNum, ComPort.ComPortSpec spec) + : base(key) + { + Port = ComDevice.ComPorts[comPortNum]; + Spec = spec; + + Debug.Console(2, "Creating com port '{0}'", key); + Debug.Console(2, "Com port spec:\r{0}", JsonConvert.SerializeObject(spec)); + } + + /// + /// Creates a ComPort if the parameters are correct. Returns and logs errors if not + /// + public static ComPortController GetComPortController(string key, + IComPorts comDevice, uint comPortNum, ComPort.ComPortSpec spec) + { + Debug.Console(1, "Creating com port '{0}'", key); + if (comDevice == null) + throw new ArgumentNullException("comDevice"); + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException("key"); + if (comPortNum > comDevice.NumberOfComPorts) + { + Debug.Console(0, "[{0}] Com port {1} out of range on {2}", + key, comPortNum, comDevice.GetType().Name); + return null; + } + var port = new ComPortController(key, comDevice, comPortNum, spec); + return port; + } + + /// + /// Registers port and sends ComSpec + /// + /// false if either register or comspec fails + public override bool CustomActivate() + { + var result = Port.Register(); + if (result != eDeviceRegistrationUnRegistrationResponse.Success) + { + Debug.Console(0, this, "Cannot register Com port: {0}", result); + return false; + } + var specResult = Port.SetComPortSpec(Spec); + if (specResult != 0) + { + Debug.Console(0, this, "Cannot set comspec"); + return false; + } + Port.SerialDataReceived += new ComPortDataReceivedEvent(Port_SerialDataReceived); + return true; + } + + void Port_SerialDataReceived(ComPort ReceivingComPort, ComPortSerialDataEventArgs args) + { + if (BytesReceived != null) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(args.SerialData); + BytesReceived(this, new GenericCommMethodReceiveBytesArgs(bytes)); + } + if(TextReceived != null) + TextReceived(this, new GenericCommMethodReceiveTextArgs(args.SerialData)); + } + + public override bool Deactivate() + { + return Port.UnRegister() == eDeviceRegistrationUnRegistrationResponse.Success; + } + + #region IBasicCommunication Members + + public void SendText(string text) + { + Port.Send(text); + } + + public void SendBytes(byte[] bytes) + { + + var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + Port.Send(text); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/GenericComm.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/GenericComm.cs new file mode 100644 index 00000000..1998f62e --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/GenericComm.cs @@ -0,0 +1,56 @@ +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.Devices; +using PepperDash.Essentials.Core.Config; + + +namespace PepperDash.Essentials.Core +{ + /// + /// Serves as a generic wrapper class for all styles of IBasicCommuncation ports + /// + public class + GenericComm : ReconfigurableDevice + { + EssentialsControlPropertiesConfig PropertiesConfig; + + public IBasicCommunication CommPort { get; private set; } + + public GenericComm(DeviceConfig config) + : base(config) + { + PropertiesConfig = CommFactory.GetControlPropertiesConfig(config); + + CommPort = CommFactory.CreateCommForDevice(config); + } + + public void SetPortConfig(string portConfig) + { + // TODO: Deserialize new EssentialsControlPropertiesConfig and handle as necessary + try + { + PropertiesConfig = JsonConvert.DeserializeObject + (portConfig.ToString()); + } + catch (Exception e) + { + Debug.Console(2, this, "Error deserializing port config: {0}", e); + } + } + + protected override void CustomSetConfig(DeviceConfig config) + { + PropertiesConfig = CommFactory.GetControlPropertiesConfig(config); + + ConfigWriter.UpdateDeviceConfig(config); + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/IRPortHelper.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/IRPortHelper.cs new file mode 100644 index 00000000..015b03ea --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Comm and IR/IRPortHelper.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharpPro; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public static class IRPortHelper + { + public static string IrDriverPathPrefix + { + get + { + return Global.FilePathPrefix + "IR" + Global.DirectorySeparator; + } + } + + /// + /// Finds either the ControlSystem or a device controller that contains IR ports and + /// returns a port from the hardware device + /// + /// + /// IrPortConfig object. The port and or filename will be empty/null + /// if valid values don't exist on config + public static IrOutPortConfig GetIrPort(JToken propsToken) + { + var control = propsToken["control"]; + if (control == null) + return null; + if (control["method"].Value() != "ir") + { + Debug.Console(0, "IRPortHelper called with non-IR properties"); + return null; + } + + var port = new IrOutPortConfig(); + + var portDevKey = control.Value("controlPortDevKey"); + var portNum = control.Value("controlPortNumber"); + if (portDevKey == null || portNum == 0) + { + Debug.Console(1, "WARNING: Properties is missing port device or port number"); + return port; + } + + IIROutputPorts irDev = null; + if (portDevKey.Equals("controlSystem", StringComparison.OrdinalIgnoreCase) + || portDevKey.Equals("processor", StringComparison.OrdinalIgnoreCase)) + irDev = Global.ControlSystem; + else + irDev = DeviceManager.GetDeviceForKey(portDevKey) as IIROutputPorts; + + if (irDev == null) + { + Debug.Console(1, "[Config] Error, device with IR ports '{0}' not found", portDevKey); + return port; + } + + if (portNum <= irDev.NumberOfIROutputPorts) // success! + { + var file = IrDriverPathPrefix + control["irFile"].Value(); + port.Port = irDev.IROutputPorts[portNum]; + port.FileName = file; + return port; // new IrOutPortConfig { Port = irDev.IROutputPorts[portNum], FileName = file }; + } + else + { + Debug.Console(1, "[Config] Error, device '{0}' IR port {1} out of range", + portDevKey, portNum); + return port; + } + } + + /// + /// Returns a ready-to-go IrOutputPortController from a DeviceConfig object. + /// + public static IrOutputPortController GetIrOutputPortController(DeviceConfig devConf) + { + var irControllerKey = devConf.Key + "-ir"; + if (devConf.Properties == null) + { + Debug.Console(0, "[{0}] WARNING: Device config does not include properties. IR will not function.", devConf.Key); + return new IrOutputPortController(irControllerKey, null, ""); + } + + var control = devConf.Properties["control"]; + if (control == null) + { + var c = new IrOutputPortController(irControllerKey, null, ""); + Debug.Console(0, c, "WARNING: Device config does not include control properties. IR will not function"); + return c; + } + + var portDevKey = control.Value("controlPortDevKey"); + var portNum = control.Value("controlPortNumber"); + IIROutputPorts irDev = null; + + if (portDevKey == null) + { + var c = new IrOutputPortController(irControllerKey, null, ""); + Debug.Console(0, c, "WARNING: control properties is missing ir device"); + return c; + } + + if (portNum == 0) + { + var c = new IrOutputPortController(irControllerKey, null, ""); + Debug.Console(0, c, "WARNING: control properties is missing ir port number"); + return c; + } + + if (portDevKey.Equals("controlSystem", StringComparison.OrdinalIgnoreCase) + || portDevKey.Equals("processor", StringComparison.OrdinalIgnoreCase)) + irDev = Global.ControlSystem; + else + irDev = DeviceManager.GetDeviceForKey(portDevKey) as IIROutputPorts; + + if (irDev == null) + { + var c = new IrOutputPortController(irControllerKey, null, ""); + Debug.Console(0, c, "WARNING: device with IR ports '{0}' not found", portDevKey); + return c; + } + + if (portNum <= irDev.NumberOfIROutputPorts) // success! + return new IrOutputPortController(irControllerKey, irDev.IROutputPorts[portNum], + IrDriverPathPrefix + control["irFile"].Value()); + else + { + var c = new IrOutputPortController(irControllerKey, null, ""); + Debug.Console(0, c, "WARNING: device '{0}' IR port {1} out of range", + portDevKey, portNum); + return c; + } + } + } + + /// + /// Wrapper to help in IR port creation + /// + public class IrOutPortConfig + { + public IROutputPort Port { get; set; } + public string FileName { get; set; } + + public IrOutPortConfig() + { + FileName = ""; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/BasicConfig.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/BasicConfig.cs new file mode 100644 index 00000000..5e71b689 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/BasicConfig.cs @@ -0,0 +1,40 @@ +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; + +namespace PepperDash.Essentials.Core.Config +{ + /// + /// Override this and splice on specific room type behavior, as well as other properties + /// + public class BasicConfig + { + [JsonProperty("info")] + public InfoConfig Info { get; set; } + + [JsonProperty("devices")] + public List Devices { get; set; } + + [JsonProperty("sourceLists")] + public Dictionary> SourceLists { get; set; } + + [JsonProperty("tieLines")] + public List TieLines { get; set; } + + /// + /// Checks SourceLists for a given list and returns it if found. Otherwise, returns null + /// + public Dictionary GetSourceListForKey(string key) + { + if (string.IsNullOrEmpty(key) || !SourceLists.ContainsKey(key)) + return null; + + return SourceLists[key]; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/ConfigPropertiesHelpers.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/ConfigPropertiesHelpers.cs new file mode 100644 index 00000000..2992a385 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/ConfigPropertiesHelpers.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Essentials.Core; +using PepperDash.Core; +using Newtonsoft.Json.Linq; + +namespace PepperDash.Essentials.Core.Config +{ + public class ConfigPropertiesHelpers + { + /// + /// Returns the value of properties.hasAudio, or false if not defined + /// + public static bool GetHasAudio(DeviceConfig deviceConfig) + { + return deviceConfig.Properties.Value("hasAudio"); + } + + /// + /// Returns the value of properties.hasControls, or false if not defined + /// + public static bool GetHasControls(DeviceConfig deviceConfig) + { + return deviceConfig.Properties.Value("hasControls"); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/DeviceConfig.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/DeviceConfig.cs new file mode 100644 index 00000000..bb95da01 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/DeviceConfig.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharpPro; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Core.Config +{ + public class DeviceConfig + { + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("uid")] + public int Uid { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("group")] + public string Group { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + [JsonConverter(typeof(DevicePropertiesConverter))] + public JToken Properties { get; set; } + } + + /// + /// + /// + public class DevicePropertiesConverter : JsonConverter + { + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(JToken); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return JToken.ReadFrom(reader); + } + + public override bool CanWrite + { + get + { + return false; + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException("SOD OFF HOSER"); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigReader.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigReader.cs new file mode 100644 index 00000000..0a288944 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigReader.cs @@ -0,0 +1,176 @@ +using System; +using System.Linq; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Config; + +namespace PepperDash.Essentials.Core.Config +{ + /// + /// Loads the ConfigObject from the file + /// + public class ConfigReader + { + public static EssentialsConfig ConfigObject { get; private set; } + + public static bool LoadConfig2() + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Loading unmerged system/template portal configuration file."); + try + { + // Check for local config file first + var filePath = Global.FilePathPrefix + ConfigWriter.LocalConfigFolder + Global.DirectorySeparator + Global.ConfigFileName; + + bool localConfigFound = false; + + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Attempting to load Local config file: '{0}'", filePath); + + // Check for local config directory first + + var configFiles = GetConfigFiles(filePath); + + if (configFiles != null) + { + if (configFiles.Length > 1) + { + Debug.Console(0, Debug.ErrorLogLevel.Error, + "****Error: Multiple Local Configuration files present. Please ensure only a single file exists and reset program.****"); + return false; + } + else if(configFiles.Length == 1) + { + localConfigFound = true; + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Found Local config file: '{0}'", filePath); + } + } + else + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, + "Local Configuration file not present.", filePath); + + } + + // Check for Portal Config + if(!localConfigFound) + { + filePath = Global.FilePathPrefix + Global.ConfigFileName; + + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Attempting to load Portal config file: '{0}'", filePath); + + configFiles = GetConfigFiles(filePath); + + if (configFiles != null) + { + if (configFiles.Length > 1) + { + Debug.Console(0, Debug.ErrorLogLevel.Error, + "****Error: Multiple Portal Configuration files present. Please ensure only a single file exists and reset program.****"); + return false; + } + else + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Found Portal config file: '{0}'", filePath); + } + } + else + { + Debug.Console(0, Debug.ErrorLogLevel.Error, + "ERROR: Portal Configuration file not present. Please load file and reset program."); + return false; + } + } + + // Get the actual file path + filePath = configFiles[0].FullName; + + // Read the file + using (StreamReader fs = new StreamReader(filePath)) + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Loading config file: '{0}'", filePath); + + if (localConfigFound) + { + ConfigObject = JObject.Parse(fs.ReadToEnd()).ToObject(); + + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Successfully Loaded Local Config"); + + return true; + } + else + { + var doubleObj = JObject.Parse(fs.ReadToEnd()); + ConfigObject = PortalConfigReader.MergeConfigs(doubleObj).ToObject(); + + // Extract SystemUrl and TemplateUrl into final config output + + if (doubleObj["system_url"] != null) + { + ConfigObject.SystemUrl = doubleObj["system_url"].Value(); + } + + if (doubleObj["template_url"] != null) + { + ConfigObject.TemplateUrl = doubleObj["template_url"].Value(); + } + } + + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Successfully Loaded Merged Config"); + + return true; + } + } + catch (Exception e) + { + Debug.Console(0, Debug.ErrorLogLevel.Error, "ERROR: Config load failed: \r{0}", e); + return false; + } + } + + /// + /// Returns all the files from the directory specified. + /// + /// + /// + public static FileInfo[] GetConfigFiles(string filePath) + { + // Get the directory + var dir = Path.GetDirectoryName(filePath); + + if (Directory.Exists(dir)) + { + Debug.Console(1, "Searching in Directory '{0}'", dir); + // Get the directory info + var dirInfo = new DirectoryInfo(dir); + + // Get the file name + var fileName = Path.GetFileName(filePath); + Debug.Console(1, "For Config Files matching: '{0}'", fileName); + + // Get the files that match from the directory + return dirInfo.GetFiles(fileName); + } + else + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, + "Directory not found: ", dir); + + return null; + } + } + + /// + /// Returns the group for a given device key in config + /// + /// + /// + public static string GetGroupForDeviceKey(string key) + { + var dev = ConfigObject.Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + return dev == null ? null : dev.Group; + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigUpdater.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigUpdater.cs new file mode 100644 index 00000000..60daaf18 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigUpdater.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Crestron.SimplSharp.Net.Http; +using Crestron.SimplSharpPro.Diagnostics; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.Config +{ + public static class ConfigUpdater + { + public static event EventHandler ConfigStatusChanged; + + public static void GetConfigFromServer(string url) + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Attempting to get new config from '{0}'", url); + + // HTTP GET + var req = new HttpClientRequest(); + + try + { + req.RequestType = RequestType.Get; + req.Url.Parse(url); + + new HttpClient().DispatchAsync(req, (r, e) => + { + if (e == HTTP_CALLBACK_ERROR.COMPLETED) + { + if (r.Code == 200) + { + var newConfig = r.ContentString; + + OnStatusUpdate(eUpdateStatus.ConfigFileReceived); + + ArchiveExistingPortalConfigs(); + + CheckForLocalConfigAndDelete(); + + WriteConfigToFile(newConfig); + + RestartProgram(); + } + else + { + Debug.Console(0, Debug.ErrorLogLevel.Error, "Config Update Process Stopped. Failed to get config file from server: {0}", r.Code); + OnStatusUpdate(eUpdateStatus.UpdateFailed); + } + } + else + Debug.Console(0, Debug.ErrorLogLevel.Error, "Request for config from Server Failed: {0}", e); + }); + } + catch (Exception e) + { + Debug.Console(1, "Error Getting Config from Server: {0}", e); + } + + } + + static void OnStatusUpdate(eUpdateStatus status) + { + var handler = ConfigStatusChanged; + + if(handler != null) + { + handler(typeof(ConfigUpdater), new ConfigStatusEventArgs(status)); + } + } + + static void WriteConfigToFile(string configData) + { + var filePath = Global.FilePathPrefix+ "configurationFile-updated.json"; + + try + { + var config = JObject.Parse(configData).ToObject(); + + ConfigWriter.WriteFile(filePath, configData); + + OnStatusUpdate(eUpdateStatus.WritingConfigFile); + } + catch (Exception e) + { + Debug.Console(1, "Error parsing new config: {0}", e); + + OnStatusUpdate(eUpdateStatus.UpdateFailed); + } + } + + /// + /// Checks for any existing portal config files and archives them + /// + static void ArchiveExistingPortalConfigs() + { + var filePath = Global.FilePathPrefix + Global.ConfigFileName; + + var configFiles = ConfigReader.GetConfigFiles(filePath); + + if (configFiles != null) + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Existing config files found. Moving to Archive folder."); + + OnStatusUpdate(eUpdateStatus.ArchivingConfigs); + + MoveFilesToArchiveFolder(configFiles); + } + else + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "No Existing config files found in '{0}'. Nothing to archive", filePath); + } + } + + /// + /// Checks for presence of archive folder and if found deletes contents. + /// Moves any config files to the archive folder and adds a .bak suffix + /// + /// + static void MoveFilesToArchiveFolder(FileInfo[] files) + { + string archiveDirectoryPath = Global.FilePathPrefix + "archive"; + + if (!Directory.Exists(archiveDirectoryPath)) + { + // Directory does not exist, create it + Directory.Create(archiveDirectoryPath); + } + else + { + // Directory exists, first clear any contents + var archivedConfigFiles = ConfigReader.GetConfigFiles(archiveDirectoryPath + Global.DirectorySeparator + Global.ConfigFileName + ".bak"); + + if(archivedConfigFiles != null || archivedConfigFiles.Length > 0) + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "{0} Existing files found in archive folder. Deleting.", archivedConfigFiles.Length); + + for (int i = 0; i < archivedConfigFiles.Length; i++ ) + { + var file = archivedConfigFiles[i]; + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Deleting archived file: '{0}'", file.FullName); + file.Delete(); + } + } + + } + + // Move any files from the program folder to the archive folder + foreach (var file in files) + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Moving config file '{0}' to archive folder", file.FullName); + + // Moves the file and appends the .bak extension + var fileDest = archiveDirectoryPath + "/" + file.Name + ".bak"; + if(!File.Exists(fileDest)) + { + file.MoveTo(fileDest); + } + else + Debug.Console(0, Debug.ErrorLogLevel.Warning, "Cannot move file to archive folder. Existing file already exists with same name: '{0}'", fileDest); + } + } + + /// + /// Checks for LocalConfig folder in file system and deletes if found + /// + static void CheckForLocalConfigAndDelete() + { + var folderPath = Global.FilePathPrefix + ConfigWriter.LocalConfigFolder; + + if (Directory.Exists(folderPath)) + { + OnStatusUpdate(eUpdateStatus.DeletingLocalConfig); + Directory.Delete(folderPath); + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Local Config Found in '{0}'. Deleting.", folderPath); + } + } + + /// + /// Connects to the processor via SSH and restarts the program + /// + static void RestartProgram() + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Attempting to Reset Program"); + + OnStatusUpdate(eUpdateStatus.RestartingProgram); + + string response = string.Empty; + + CrestronConsole.SendControlSystemCommand(string.Format("progreset -p:{0}", InitialParametersClass.ApplicationNumber), ref response); + + Debug.Console(1, "Console Response: {0}", response); + } + + } + + public enum eUpdateStatus + { + UpdateStarted, + ConfigFileReceived, + ArchivingConfigs, + DeletingLocalConfig, + WritingConfigFile, + RestartingProgram, + UpdateSucceeded, + UpdateFailed + } + + public class ConfigStatusEventArgs : EventArgs + { + public eUpdateStatus UpdateStatus { get; private set; } + + public ConfigStatusEventArgs(eUpdateStatus status) + { + UpdateStatus = status; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigWriter.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigWriter.cs new file mode 100644 index 00000000..1cd7b93e --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/ConfigWriter.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.Config +{ + /// + /// Responsible for updating config at runtime, and writing the updates out to a local file + /// + public class ConfigWriter + { + public const string LocalConfigFolder = "LocalConfig"; + + public const long WriteTimeout = 30000; + + public static CTimer WriteTimer; + + /// + /// Updates the config properties of a device + /// + /// + /// + /// + public static bool UpdateDeviceProperties(string deviceKey, JToken properties) + { + bool success = false; + + // Get the current device config + var deviceConfig = ConfigReader.ConfigObject.Devices.FirstOrDefault(d => d.Key.Equals(deviceKey)); + + if (deviceConfig != null) + { + // Replace the current properties JToken with the new one passed into this method + deviceConfig.Properties = properties; + + Debug.Console(1, "Updated properties of device: '{0}'", deviceKey); + + success = true; + } + + ResetTimer(); + + return success; + } + + public static bool UpdateDeviceConfig(DeviceConfig config) + { + bool success = false; + + var deviceConfig = ConfigReader.ConfigObject.Devices.FirstOrDefault(d => d.Key.Equals(config.Key)); + + if (deviceConfig != null) + { + deviceConfig = config; + + Debug.Console(1, "Updated config of device: '{0}'", config.Key); + + success = true; + } + + ResetTimer(); + + return success; + } + + public static bool UpdateRoomConfig(DeviceConfig config) + { + bool success = false; + + var deviceConfig = ConfigReader.ConfigObject.Rooms.FirstOrDefault(d => d.Key.Equals(config.Key)); + + if (deviceConfig != null) + { + deviceConfig = config; + + Debug.Console(1, "Updated config of device: '{0}'", config.Key); + + success = true; + } + + ResetTimer(); + + return success; + } + + /// + /// Resets (or starts) the write timer + /// + static void ResetTimer() + { + if (WriteTimer == null) + WriteTimer = new CTimer(WriteConfigFile, WriteTimeout); + + WriteTimer.Reset(WriteTimeout); + + Debug.Console(1, "Config File write timer has been reset."); + } + + /// + /// Writes the current config to a file in the LocalConfig subfolder + /// + /// + private static void WriteConfigFile(object o) + { + var filePath = Global.FilePathPrefix + LocalConfigFolder + Global.DirectorySeparator + "configurationFile.json"; + + var configData = JsonConvert.SerializeObject(ConfigReader.ConfigObject); + + WriteFile(filePath, configData); + } + + /// + /// Writes + /// + /// + /// + public static void WriteFile(string filePath, string configData) + { + if (WriteTimer != null) + WriteTimer.Stop(); + + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Writing Configuration to file"); + + var fileLock = new CCriticalSection(); + + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Attempting to write config file: '{0}'", filePath); + + try + { + if (fileLock.TryEnter()) + { + using (StreamWriter sw = new StreamWriter(filePath)) + { + sw.Write(configData); + sw.Flush(); + } + } + else + { + Debug.Console(0, Debug.ErrorLogLevel.Error, "Unable to enter FileLock"); + } + } + catch (Exception e) + { + Debug.Console(0, Debug.ErrorLogLevel.Error, "Error: Config write failed: \r{0}", e); + } + finally + { + if (fileLock != null && !fileLock.Disposed) + fileLock.Leave(); + + } + } + + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/EssentialsConfig.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/EssentialsConfig.cs new file mode 100644 index 00000000..69f162d0 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/Essentials/EssentialsConfig.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +using Crestron.SimplSharp.CrestronIO; +using Newtonsoft.Json; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.Config +{ + /// + /// Loads the ConfigObject from the file + /// + public class EssentialsConfig : BasicConfig + { + [JsonProperty("system_url")] + public string SystemUrl { get; set; } + + [JsonProperty("template_url")] + public string TemplateUrl { get; set; } + + + //public CotijaConfig Cotija { get; private set; } + + [JsonProperty("systemUuid")] + public string SystemUuid + { + get + { + if (string.IsNullOrEmpty(SystemUrl)) + return "missing url"; + + var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*"); + string uuid = result.Groups[1].Value; + return uuid; + } + } + + [JsonProperty("templateUuid")] + public string TemplateUuid + { + get + { + if (string.IsNullOrEmpty(TemplateUrl)) + return "missing template url"; + + var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*"); + string uuid = result.Groups[1].Value; + return uuid; + } + } + + [JsonProperty("rooms")] + public List Rooms { get; set; } + } + + /// + /// + /// + public class SystemTemplateConfigs + { + public EssentialsConfig System { get; set; } + + public EssentialsConfig Template { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/InfoConfig.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/InfoConfig.cs new file mode 100644 index 00000000..d5cf16f1 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/InfoConfig.cs @@ -0,0 +1,96 @@ +using System; + +using Crestron.SimplSharp; +using Crestron.SimplSharp.Reflection; + +using Newtonsoft.Json; + +namespace PepperDash.Essentials.Core.Config +{ + /// + /// Represents the info section of a Config file + /// + public class InfoConfig + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("date")] + public DateTime Date { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } + + [JsonProperty("runtimeInfo")] + public RuntimeInfo RuntimeInfo { get; set; } + + [JsonProperty("comment")] + public string Comment { get; set; } + + [JsonProperty("hostname")] + public string HostName { get; set; } + + [JsonProperty("appNumber")] + public uint AppNumber { get; set; } + + public InfoConfig() + { + Name = ""; + Date = DateTime.Now; + Type = ""; + Version = ""; + Comment = ""; + HostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); + AppNumber = InitialParametersClass.ApplicationNumber; + + RuntimeInfo = new RuntimeInfo(); + } + } + + + /// + /// Represents runtime information about the program/processor + /// + public class RuntimeInfo + { + /// + /// The name of the running application + /// + [JsonProperty("appName")] + public string AppName {get; set;} + //{ + // get + // { + // return Assembly.GetExecutingAssembly().GetName().Name; + // } + //} + + /// + /// The Assembly version of the running application + /// + [JsonProperty("assemblyVersion")] + public string AssemblyVersion {get; set;} + //{ + // get + // { + // var version = Assembly.GetExecutingAssembly().GetName().Version; + // return string.Format("{0}.{1}.{2}", version.Major, version.Minor, version.Build); + // } + //} + + /// + /// The OS Version of the processor (Firmware Version) + /// + [JsonProperty("osVersion")] + public string OsVersion {get; set;} + //{ + // get + // { + // return Crestron.SimplSharp.CrestronEnvironment.OSVersion.Firmware; + // } + //} + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/SourceDevicePropertiesConfigBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/SourceDevicePropertiesConfigBase.cs new file mode 100644 index 00000000..e6d13339 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Config/SourceDevicePropertiesConfigBase.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core.Config +{ + public class SourceDevicePropertiesConfigBase + { + public bool DisableSharing { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Constants/CommonCues.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Constants/CommonCues.cs new file mode 100644 index 00000000..fceb9ad7 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Constants/CommonCues.cs @@ -0,0 +1,127 @@ +using Crestron.SimplSharpPro; + +namespace PepperDash.Essentials.Core +{ + public static class CommonBoolCue + { + public static readonly Cue Power = new Cue("Power", 101, eCueType.Bool); + public static readonly Cue PowerOn = new Cue("PowerOn", 102, eCueType.Bool); + public static readonly Cue PowerOff = new Cue("PowerOff", 103, eCueType.Bool); + + public static readonly Cue HasPowerFeedback = new Cue("HasPowerFeedback", 101, eCueType.Bool); + public static readonly Cue PowerOnFeedback = new Cue("PowerOnFeedback", 102, eCueType.Bool); + public static readonly Cue IsOnlineFeedback = new Cue("IsOnlineFeedback", 104, eCueType.Bool); + public static readonly Cue IsWarmingUp = new Cue("IsWarmingUp", 105, eCueType.Bool); + public static readonly Cue IsCoolingDown = new Cue("IsCoolingDown", 106, eCueType.Bool); + + public static readonly Cue Dash = new Cue("Dash", 109, eCueType.Bool); + public static readonly Cue Digit0 = new Cue("Digit0", 110, eCueType.Bool); + public static readonly Cue Digit1 = new Cue("Digit1", 111, eCueType.Bool); + public static readonly Cue Digit2 = new Cue("Digit2", 112, eCueType.Bool); + public static readonly Cue Digit3 = new Cue("Digit3", 113, eCueType.Bool); + public static readonly Cue Digit4 = new Cue("Digit4", 114, eCueType.Bool); + public static readonly Cue Digit5 = new Cue("Digit5", 115, eCueType.Bool); + public static readonly Cue Digit6 = new Cue("Digit6", 116, eCueType.Bool); + public static readonly Cue Digit7 = new Cue("Digit7", 117, eCueType.Bool); + public static readonly Cue Digit8 = new Cue("Digit8", 118, eCueType.Bool); + public static readonly Cue Digit9 = new Cue("Digit9", 119, eCueType.Bool); + public static readonly Cue KeypadMisc1 = new Cue("KeypadMisc1", 120, eCueType.Bool); + public static readonly Cue KeypadMisc2 = new Cue("KeypadMisc2", 121, eCueType.Bool); + + public static readonly Cue NumericEnter = new Cue("Enter", 122, eCueType.Bool); + public static readonly Cue ChannelUp = new Cue("ChannelUp", 123, eCueType.Bool); + public static readonly Cue ChannelDown = new Cue("ChannelDown", 124, eCueType.Bool); + public static readonly Cue Last = new Cue("Last", 125, eCueType.Bool); + public static readonly Cue OpenClose = new Cue("OpenClose", 126, eCueType.Bool); + public static readonly Cue Subtitle = new Cue("Subtitle", 127, eCueType.Bool); + public static readonly Cue Audio = new Cue("Audio", 128, eCueType.Bool); + public static readonly Cue Info = new Cue("Info", 129, eCueType.Bool); + public static readonly Cue Menu = new Cue("Menu", 130, eCueType.Bool); + public static readonly Cue DeviceMenu = new Cue("DeviceMenu", 131, eCueType.Bool); + public static readonly Cue Return = new Cue("Return", 132, eCueType.Bool); + public static readonly Cue Back = new Cue("Back", 133, eCueType.Bool); + public static readonly Cue Exit = new Cue("Exit", 134, eCueType.Bool); + public static readonly Cue Clear = new Cue("Clear", 135, eCueType.Bool); + public static readonly Cue List = new Cue("List", 136, eCueType.Bool); + public static readonly Cue Guide = new Cue("Guide", 137, eCueType.Bool); + public static readonly Cue Am = new Cue("Am", 136, eCueType.Bool); + public static readonly Cue Fm = new Cue("Fm", 137, eCueType.Bool); + public static readonly Cue Up = new Cue("Up", 138, eCueType.Bool); + public static readonly Cue Down = new Cue("Down", 139, eCueType.Bool); + public static readonly Cue Left = new Cue("Left", 140, eCueType.Bool); + public static readonly Cue Right = new Cue("Right", 141, eCueType.Bool); + public static readonly Cue Select = new Cue("Select", 142, eCueType.Bool); + public static readonly Cue SmartApps = new Cue("SmartApps", 143, eCueType.Bool); + public static readonly Cue Dvr = new Cue("Dvr", 144, eCueType.Bool); + + public static readonly Cue Play = new Cue("Play", 145, eCueType.Bool); + public static readonly Cue Pause = new Cue("Pause", 146, eCueType.Bool); + public static readonly Cue Stop = new Cue("Stop", 147, eCueType.Bool); + public static readonly Cue ChapNext = new Cue("ChapNext", 148, eCueType.Bool); + public static readonly Cue ChapPrevious = new Cue("ChapPrevious", 149, eCueType.Bool); + public static readonly Cue Rewind = new Cue("Rewind", 150, eCueType.Bool); + public static readonly Cue Ffwd = new Cue("Ffwd", 151, eCueType.Bool); + public static readonly Cue Replay = new Cue("Replay", 152, eCueType.Bool); + public static readonly Cue Advance = new Cue("Advance", 153, eCueType.Bool); + public static readonly Cue Record = new Cue("Record", 154, eCueType.Bool); + public static readonly Cue Red = new Cue("Red", 155, eCueType.Bool); + public static readonly Cue Green = new Cue("Green", 156, eCueType.Bool); + public static readonly Cue Yellow = new Cue("Yellow", 157, eCueType.Bool); + public static readonly Cue Blue = new Cue("Blue", 158, eCueType.Bool); + public static readonly Cue Home = new Cue("Home", 159, eCueType.Bool); + public static readonly Cue PopUp = new Cue("PopUp", 160, eCueType.Bool); + public static readonly Cue PageUp = new Cue("PageUp", 161, eCueType.Bool); + public static readonly Cue PageDown = new Cue("PageDown", 162, eCueType.Bool); + public static readonly Cue Search = new Cue("Search", 163, eCueType.Bool); + public static readonly Cue Setup = new Cue("Setup", 164, eCueType.Bool); + public static readonly Cue RStep = new Cue("RStep", 165, eCueType.Bool); + public static readonly Cue FStep = new Cue("FStep", 166, eCueType.Bool); + + public static readonly Cue IsConnected = new Cue("IsConnected", 281, eCueType.Bool); + public static readonly Cue IsOk = new Cue("IsOk", 282, eCueType.Bool); + public static readonly Cue InWarning = new Cue("InWarning", 283, eCueType.Bool); + public static readonly Cue InError = new Cue("InError", 284, eCueType.Bool); + public static readonly Cue StatusUnknown = new Cue("StatusUnknown", 285, eCueType.Bool); + + public static readonly Cue VolumeUp = new Cue("VolumeUp", 401, eCueType.Bool); + public static readonly Cue VolumeDown = new Cue("VolumeDown", 402, eCueType.Bool); + public static readonly Cue MuteOn = new Cue("MuteOn", 403, eCueType.Bool); + public static readonly Cue MuteOff = new Cue("MuteOff", 404, eCueType.Bool); + public static readonly Cue MuteToggle = new Cue("MuteToggle", 405, eCueType.Bool); + public static readonly Cue ShowVolumeButtons = new Cue("ShowVolumeButtons", 406, eCueType.Bool); + public static readonly Cue ShowVolumeSlider = new Cue("ShowVolumeSlider", 407, eCueType.Bool); + + public static readonly Cue Hdmi1 = new Cue("Hdmi1", 451, eCueType.Bool); + public static readonly Cue Hdmi2 = new Cue("Hdmi2", 452, eCueType.Bool); + public static readonly Cue Hdmi3 = new Cue("Hdmi3", 453, eCueType.Bool); + public static readonly Cue Hdmi4 = new Cue("Hdmi4", 454, eCueType.Bool); + public static readonly Cue Hdmi5 = new Cue("Hdmi5", 455, eCueType.Bool); + public static readonly Cue Hdmi6 = new Cue("Hdmi6", 456, eCueType.Bool); + public static readonly Cue DisplayPort1 = new Cue("DisplayPort1", 457, eCueType.Bool); + public static readonly Cue DisplayPort2 = new Cue("DisplayPort2", 458, eCueType.Bool); + public static readonly Cue Dvi1 = new Cue("Dvi1", 459, eCueType.Bool); + public static readonly Cue Dvi2 = new Cue("Dvi2", 460, eCueType.Bool); + public static readonly Cue Video1 = new Cue("Video1", 461, eCueType.Bool); + public static readonly Cue Video2 = new Cue("Video2", 462, eCueType.Bool); + public static readonly Cue Component1 = new Cue("Component1", 463, eCueType.Bool); + public static readonly Cue Component2 = new Cue("Component2", 464, eCueType.Bool); + public static readonly Cue Vga1 = new Cue("Vga1", 465, eCueType.Bool); + public static readonly Cue Vga2 = new Cue("Vga2", 466, eCueType.Bool); + public static readonly Cue Rgb1 = new Cue("Rgb1", 467, eCueType.Bool); + public static readonly Cue Rgb2 = new Cue("Rgb2", 468, eCueType.Bool); + public static readonly Cue Antenna = new Cue("Antenna", 469, eCueType.Bool); + + public static readonly Cue InCall = new Cue("InCall", 501, eCueType.Bool); + } + + public static class CommonIntCue + { + public static readonly Cue MainVolumeLevel = new Cue("MainVolumeLevel", 401, eCueType.Int); + public static readonly Cue MainVolumeLevelFeedback = new Cue("MainVolumeLevelFeedback", 401, eCueType.Int); + } + + public static class CommonStringCue + { + public static readonly Cue IpConnectionsText = new Cue("IpConnectionsText", 9999, eCueType.String); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/IOPortConfig.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/IOPortConfig.cs new file mode 100644 index 00000000..09061ff2 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/IOPortConfig.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Newtonsoft.Json; + +namespace PepperDash.Essentials.Core.CrestronIO +{ + public class IOPortConfig + { + [JsonProperty("portDeviceKey")] + public string PortDeviceKey { get; set; } + [JsonProperty("portNumber")] + public uint PortNumber { get; set; } + [JsonProperty("disablePullUpResistor")] + public bool DisablePullUpResistor { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/GenericDigitalInputDevice.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/GenericDigitalInputDevice.cs new file mode 100644 index 00000000..3c1849f9 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/GenericDigitalInputDevice.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.CrestronIO +{ + public class GenericDigitalInputDevice : Device, IDigitalInput + { + public DigitalInput InputPort { get; private set; } + + public BoolFeedback InputStateFeedback { get; private set; } + + Func InputStateFeedbackFunc + { + get + { + return () => InputPort.State; + } + } + + public GenericDigitalInputDevice(string key, DigitalInput inputPort): + base(key) + { + InputStateFeedback = new BoolFeedback(InputStateFeedbackFunc); + + InputPort = inputPort; + + InputPort.StateChange += new DigitalInputEventHandler(InputPort_StateChange); + + } + + void InputPort_StateChange(DigitalInput digitalInput, DigitalInputEventArgs args) + { + InputStateFeedback.FireUpdate(); + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/GenericVersiportInputDevice.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/GenericVersiportInputDevice.cs new file mode 100644 index 00000000..4c5359b9 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/GenericVersiportInputDevice.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.CrestronIO +{ + /// + /// Represents a generic digital input deviced tied to a versiport + /// + public class GenericVersiportDigitalInputDevice : Device, IDigitalInput + { + public Versiport InputPort { get; private set; } + + public BoolFeedback InputStateFeedback { get; private set; } + + Func InputStateFeedbackFunc + { + get + { + return () => InputPort.DigitalIn; + } + } + + public GenericVersiportDigitalInputDevice(string key, Versiport inputPort, IOPortConfig props): + base(key) + { + InputStateFeedback = new BoolFeedback(InputStateFeedbackFunc); + InputPort = inputPort; + InputPort.SetVersiportConfiguration(eVersiportConfiguration.DigitalInput); + if (props.DisablePullUpResistor) + InputPort.DisablePullUpResistor = true; + InputPort.VersiportChange += new VersiportEventHandler(InputPort_VersiportChange); + + Debug.Console(1, this, "Created GenericVersiportDigitalInputDevice on port '{0}'. DisablePullUpResistor: '{1}'", props.PortNumber, InputPort.DisablePullUpResistor); + } + + void InputPort_VersiportChange(Versiport port, VersiportEventArgs args) + { + Debug.Console(1, this, "Versiport change: {0}", args.Event); + + if(args.Event == eVersiportEvent.DigitalInChange) + InputStateFeedback.FireUpdate(); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/IDigitalInput.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/IDigitalInput.cs new file mode 100644 index 00000000..7c63b92c --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Inputs/IDigitalInput.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core.CrestronIO +{ + /// + /// Represents a device that provides digital input + /// + public interface IDigitalInput + { + BoolFeedback InputStateFeedback { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Relay/GenericRelayDevice.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Relay/GenericRelayDevice.cs new file mode 100644 index 00000000..4786cb2b --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Relay/GenericRelayDevice.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.CrestronIO +{ + /// + /// Represents a generic device controlled by relays + /// + public class GenericRelayDevice : Device, ISwitchedOutput + { + public Relay RelayOutput { get; private set; } + + public BoolFeedback OutputIsOnFeedback { get; private set; } + + public GenericRelayDevice(string key, Relay relay): + base(key) + { + OutputIsOnFeedback = new BoolFeedback(new Func(() => RelayOutput.State)); + + RelayOutput = relay; + RelayOutput.Register(); + + RelayOutput.StateChange += new RelayEventHandler(RelayOutput_StateChange); + } + + void RelayOutput_StateChange(Relay relay, RelayEventArgs args) + { + OutputIsOnFeedback.FireUpdate(); + } + + public void OpenRelay() + { + RelayOutput.State = false; + } + + public void CloseRelay() + { + RelayOutput.State = true; + } + + public void ToggleRelayState() + { + if (RelayOutput.State == true) + OpenRelay(); + else + CloseRelay(); + } + + #region ISwitchedOutput Members + + void ISwitchedOutput.On() + { + CloseRelay(); + } + + void ISwitchedOutput.Off() + { + OpenRelay(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Relay/ISwitchedOutput.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Relay/ISwitchedOutput.cs new file mode 100644 index 00000000..19f8e0df --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/Relay/ISwitchedOutput.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Core.CrestronIO +{ + /// + /// Describes an output capable of switching on and off + /// + public interface ISwitchedOutput + { + BoolFeedback OutputIsOnFeedback {get;} + + void On(); + void Off(); + } + + public interface ISwitchedOutputCollection + { + Dictionary SwitchedOutputs { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron/CrestronGenericBaseDevice.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron/CrestronGenericBaseDevice.cs new file mode 100644 index 00000000..08223e03 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron/CrestronGenericBaseDevice.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// A bridge class to cover the basic features of GenericBase hardware + /// + public class CrestronGenericBaseDevice : Device, IOnline, IHasFeedback, ICommunicationMonitor, IUsageTracking + { + public virtual GenericBase Hardware { get; protected set; } + + /// + /// Returns a list containing the Outputs that we want to expose. + /// + public FeedbackCollection Feedbacks { get; private set; } + + public BoolFeedback IsOnline { get; private set; } + public BoolFeedback IsRegistered { get; private set; } + public StringFeedback IpConnectionsText { get; private set; } + + /// + /// Used by implementing classes to prevent registration with Crestron TLDM. For + /// devices like RMCs and TXs attached to a chassis. + /// + public bool PreventRegistration { get; protected set; } + + public CrestronGenericBaseDevice(string key, string name, GenericBase hardware) + : base(key, name) + { + Feedbacks = new FeedbackCollection(); + + Hardware = hardware; + IsOnline = new BoolFeedback("IsOnlineFeedback", () => Hardware.IsOnline); + IsRegistered = new BoolFeedback("IsRegistered", () => Hardware.Registered); + IpConnectionsText = new StringFeedback("IpConnectionsText", () => + string.Join(",", Hardware.ConnectedIpList.Select(cip => cip.DeviceIpAddress).ToArray())); + + AddToFeedbackList(IsOnline, IsRegistered, IpConnectionsText); + + CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, hardware, 120000, 300000); + } + + /// + /// Make sure that overriding classes call this! + /// Registers the Crestron device, connects up to the base events, starts communication monitor + /// + public override bool CustomActivate() + { + Debug.Console(0, this, "Activating"); + if (!PreventRegistration) + { + //Debug.Console(1, this, " Does not require registration. Skipping"); + + var response = Hardware.RegisterWithLogging(Key); + if (response != eDeviceRegistrationUnRegistrationResponse.Success) + { + //Debug.Console(0, this, "ERROR: Cannot register Crestron device: {0}", response); + return false; + } + } + + Hardware.OnlineStatusChange += new OnlineStatusChangeEventHandler(Hardware_OnlineStatusChange); + CommunicationMonitor.Start(); + + return true; + } + + /// + /// This disconnects events and unregisters the base hardware device. + /// + /// + public override bool Deactivate() + { + CommunicationMonitor.Stop(); + Hardware.OnlineStatusChange -= Hardware_OnlineStatusChange; + + return Hardware.UnRegister() == eDeviceRegistrationUnRegistrationResponse.Success; + } + + /// + /// Adds feedback(s) to the list + /// + /// + public void AddToFeedbackList(params Feedback[] newFbs) + { + foreach (var f in newFbs) + { + if (f != null) + { + if (!Feedbacks.Contains(f)) + { + Feedbacks.Add(f); + } + } + } + } + + void Hardware_OnlineStatusChange(GenericBase currentDevice, OnlineOfflineEventArgs args) + { + if (args.DeviceOnLine) + { + foreach (var feedback in Feedbacks) + { + if (feedback != null) + feedback.FireUpdate(); + } + } + } + + #region IStatusMonitor Members + + public StatusMonitorBase CommunicationMonitor { get; private set; } + #endregion + + #region IUsageTracking Members + + public UsageTracking UsageTracker { get; set; } + + #endregion + } + + //*********************************************************************************** + public class CrestronGenericBaseDeviceEventIds + { + public const uint IsOnline = 1; + public const uint IpConnectionsText =2; + } + + /// + /// Adds logging to Register() failure + /// + public static class GenericBaseExtensions + { + public static eDeviceRegistrationUnRegistrationResponse RegisterWithLogging(this GenericBase device, string key) + { + var result = device.Register(); + var level = result == eDeviceRegistrationUnRegistrationResponse.Success ? + Debug.ErrorLogLevel.Notice : Debug.ErrorLogLevel.Error; + Debug.Console(0, level, "Register device result: '{0}', type '{1}', result {2}", key, device, result); + //if (result != eDeviceRegistrationUnRegistrationResponse.Success) + //{ + // Debug.Console(0, Debug.ErrorLogLevel.Error, "Cannot register device '{0}': {1}", key, result); + //} + return result; + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron/CrestronGenericBaseDevice.cs.orig b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron/CrestronGenericBaseDevice.cs.orig new file mode 100644 index 00000000..416cc553 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron/CrestronGenericBaseDevice.cs.orig @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// A bridge class to cover the basic features of GenericBase hardware + /// + public class CrestronGenericBaseDevice : Device, IOnline, IHasFeedback, ICommunicationMonitor, IUsageTracking + { + public virtual GenericBase Hardware { get; protected set; } + + public BoolFeedback IsOnline { get; private set; } + public BoolFeedback IsRegistered { get; private set; } + public StringFeedback IpConnectionsText { get; private set; } + + public CrestronGenericBaseDevice(string key, string name, GenericBase hardware) + : base(key, name) + { + Hardware = hardware; + IsOnline = new BoolFeedback(CommonBoolCue.IsOnlineFeedback, () => Hardware.IsOnline); + IsRegistered = new BoolFeedback(new Cue("IsRegistered", 0, eCueType.Bool), () => Hardware.Registered); + IpConnectionsText = new StringFeedback(CommonStringCue.IpConnectionsText, () => + string.Join(",", Hardware.ConnectedIpList.Select(cip => cip.DeviceIpAddress).ToArray())); + CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, hardware, 120000, 300000); + } + + /// + /// Make sure that overriding classes call this! + /// Registers the Crestron device, connects up to the base events, starts communication monitor + /// + public override bool CustomActivate() + { + Debug.Console(0, this, "Activating"); + var response = Hardware.RegisterWithLogging(Key); + if (response != eDeviceRegistrationUnRegistrationResponse.Success) + { +<<<<<<< HEAD + Debug.Console(0, this, "ERROR: Cannot register Crestron device: {0}", response); + return false; + } +======= + Debug.Console(0, this, "ERROR: Cannot register Crestron device: {0}", response); + return false; + } +>>>>>>> origin/feature/ecs-342-neil + Hardware.OnlineStatusChange += new OnlineStatusChangeEventHandler(Hardware_OnlineStatusChange); + CommunicationMonitor.Start(); + + return true; + } + + /// + /// This disconnects events and unregisters the base hardware device. + /// + /// + public override bool Deactivate() + { + CommunicationMonitor.Stop(); + Hardware.OnlineStatusChange -= Hardware_OnlineStatusChange; + + return Hardware.UnRegister() == eDeviceRegistrationUnRegistrationResponse.Success; + } + + /// + /// Returns a list containing the Outputs that we want to expose. + /// + public virtual List Feedbacks + { + get + { + return new List + { + IsOnline, + IsRegistered, + IpConnectionsText + }; + } + } + + void Hardware_OnlineStatusChange(GenericBase currentDevice, OnlineOfflineEventArgs args) + { + IsOnline.FireUpdate(); + } + + #region IStatusMonitor Members + + public StatusMonitorBase CommunicationMonitor { get; private set; } + #endregion + + #region IUsageTracking Members + + public UsageTracking UsageTracker { get; set; } + + #endregion + } + + //*********************************************************************************** + public class CrestronGenericBaseDeviceEventIds + { + public const uint IsOnline = 1; + public const uint IpConnectionsText =2; + } + + /// + /// Adds logging to Register() failure + /// + public static class GenericBaseExtensions + { + public static eDeviceRegistrationUnRegistrationResponse RegisterWithLogging(this GenericBase device, string key) + { + var result = device.Register(); + if (result != eDeviceRegistrationUnRegistrationResponse.Success) + { + Debug.Console(0, Debug.ErrorLogLevel.Error, "Cannot register device '{0}': {1}", key, result); + } + return result; + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Cues and DevAction/Cues.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Cues and DevAction/Cues.cs new file mode 100644 index 00000000..a3940a98 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Cues and DevAction/Cues.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + + +namespace PepperDash.Essentials.Core +{ + /// + /// Encapsulates a string-named, joined and typed command to a device + /// + [Obsolete()] + public class DevAction + { + public Cue Cue { get; private set; } + public Action Action { get; private set; } + + + public DevAction(Cue cue, Action action) + { + Cue = cue; + Action = action; + } + } + + public enum eCueType + { + Bool, Int, String, Serial, Void, Other + } + + + /// + /// The Cue class is a container to represent a name / join number / type for simplifying + /// commands coming into devices. + /// + public class Cue + { + public string Name { get; private set; } + public uint Number { get; private set; } + public eCueType Type { get; private set; } + + public Cue(string name, uint join, eCueType type) + { + Name = name; + Number = join; + Type = type; + } + + /// + /// Override that prints out the cue's data + /// + public override string ToString() + { + return string.Format("{0} Cue '{1}'-{2}", Type, Name, Number); + } + + ///// + ///// Returns a new Cue with JoinNumber offset + ///// + //public Cue GetOffsetCopy(uint offset) + //{ + // return new Cue(Name, Number + offset, Type); + //} + + /// + /// Helper method to create a Cue of Bool type + /// + /// Cue + public static Cue BoolCue(string name, uint join) + { + return new Cue(name, join, eCueType.Bool); + } + + /// + /// Helper method to create a Cue of ushort type + /// + /// Cue + public static Cue UShortCue(string name, uint join) + { + return new Cue(name, join, eCueType.Int); + } + + /// + /// Helper method to create a Cue of string type + /// + /// Cue + public static Cue StringCue(string name, uint join) + { + return new Cue(name, join, eCueType.String); + } + + public static readonly Cue DefaultBoolCue = new Cue("-none-", 0, eCueType.Bool); + public static readonly Cue DefaultIntCue = new Cue("-none-", 0, eCueType.Int); + public static readonly Cue DefaultStringCue = new Cue("-none-", 0, eCueType.String); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceControlsParentInterfaces/IPresentationSource.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceControlsParentInterfaces/IPresentationSource.cs new file mode 100644 index 00000000..ac45a9d4 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceControlsParentInterfaces/IPresentationSource.cs @@ -0,0 +1,19 @@ +//using System; +//using System.Collections.Generic; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; + +//using PepperDash.Core; + + +//namespace PepperDash.Essentials.Core +//{ +// public interface IPresentationSource : IKeyed +// { +// string Name { get; } +// PresentationSourceType Type { get; } +// string IconName { get; set; } +// BoolFeedback HasPowerOnFeedback { get; } + +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IChannel.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IChannel.cs new file mode 100644 index 00000000..5a38c703 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IChannel.cs @@ -0,0 +1,47 @@ +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.SmartObjects; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public interface IChannel + { + void ChannelUp(bool pressRelease); + void ChannelDown(bool pressRelease); + void LastChannel(bool pressRelease); + void Guide(bool pressRelease); + void Info(bool pressRelease); + void Exit(bool pressRelease); + } + + /// + /// + /// + public static class IChannelExtensions + { + public static void LinkButtons(this IChannel dev, BasicTriList triList) + { + triList.SetBoolSigAction(123, dev.ChannelUp); + triList.SetBoolSigAction(124, dev.ChannelDown); + triList.SetBoolSigAction(125, dev.LastChannel); + triList.SetBoolSigAction(137, dev.Guide); + triList.SetBoolSigAction(129, dev.Info); + triList.SetBoolSigAction(134, dev.Exit); + } + + public static void UnlinkButtons(this IChannel dev, BasicTriList triList) + { + triList.ClearBoolSigAction(123); + triList.ClearBoolSigAction(124); + triList.ClearBoolSigAction(125); + triList.ClearBoolSigAction(137); + triList.ClearBoolSigAction(129); + triList.ClearBoolSigAction(134); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IColorFunctions.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IColorFunctions.cs new file mode 100644 index 00000000..232362e9 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IColorFunctions.cs @@ -0,0 +1,41 @@ +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.SmartObjects; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public interface IColor + { + void Red(bool pressRelease); + void Green(bool pressRelease); + void Yellow(bool pressRelease); + void Blue(bool pressRelease); + } + + /// + /// + /// + public static class IColorExtensions + { + public static void LinkButtons(this IColor dev, BasicTriList TriList) + { + TriList.SetBoolSigAction(155, dev.Red); + TriList.SetBoolSigAction(156, dev.Green); + TriList.SetBoolSigAction(157, dev.Yellow); + TriList.SetBoolSigAction(158, dev.Blue); + } + + public static void UnlinkButtons(this IColor dev, BasicTriList triList) + { + triList.ClearBoolSigAction(155); + triList.ClearBoolSigAction(156); + triList.ClearBoolSigAction(157); + triList.ClearBoolSigAction(158); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDPad.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDPad.cs new file mode 100644 index 00000000..2e0f3ae7 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDPad.cs @@ -0,0 +1,50 @@ +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.SmartObjects; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public interface IDPad + { + void Up(bool pressRelease); + void Down(bool pressRelease); + void Left(bool pressRelease); + void Right(bool pressRelease); + void Select(bool pressRelease); + void Menu(bool pressRelease); + void Exit(bool pressRelease); + } + + /// + /// + /// + public static class IDPadExtensions + { + public static void LinkButtons(this IDPad dev, BasicTriList triList) + { + triList.SetBoolSigAction(138, dev.Up); + triList.SetBoolSigAction(139, dev.Down); + triList.SetBoolSigAction(140, dev.Left); + triList.SetBoolSigAction(141, dev.Right); + triList.SetBoolSigAction(142, dev.Select); + triList.SetBoolSigAction(130, dev.Menu); + triList.SetBoolSigAction(134, dev.Exit); + } + + public static void UnlinkButtons(this IDPad dev, BasicTriList triList) + { + triList.ClearBoolSigAction(138); + triList.ClearBoolSigAction(139); + triList.ClearBoolSigAction(140); + triList.ClearBoolSigAction(141); + triList.ClearBoolSigAction(142); + triList.ClearBoolSigAction(130); + triList.ClearBoolSigAction(134); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDiscPlayerControls.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDiscPlayerControls.cs new file mode 100644 index 00000000..31fb83b4 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDiscPlayerControls.cs @@ -0,0 +1,13 @@ +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.SmartObjects; + +namespace PepperDash.Essentials.Core +{ + + public interface IDiscPlayerControls : IColor, IDPad, INumericKeypad, IPower, ITransport, IUiDisplayInfo + { + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDisplayBasic.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDisplayBasic.cs new file mode 100644 index 00000000..5624f77c --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDisplayBasic.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core.Devices.DeviceTypeInterfaces +{ + public interface IDisplayBasic + { + void InputHdmi1(); + void InputHdmi2(); + void InputHdmi3(); + void InputHdmi4(); + void InputDisplayPort1(); + void InputDvi1(); + void InputVideo1(); + void InputVga1(); + void InputVga2(); + void InputRgb1(); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDumbSource.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDumbSource.cs new file mode 100644 index 00000000..1d4829cd --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDumbSource.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + public interface IDumbSource + { + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDvr.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDvr.cs new file mode 100644 index 00000000..9e55702c --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IDvr.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.SmartObjects; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public interface IDvr : IDPad + { + void DvrList(bool pressRelease); + void Record(bool pressRelease); + } + + /// + /// + /// + public static class IDvrExtensions + { + public static void LinkButtons(this IDvr dev, BasicTriList triList) + { + triList.SetBoolSigAction(136, dev.DvrList); + triList.SetBoolSigAction(152, dev.Record); + } + + public static void UnlinkButtons(this IDvr dev, BasicTriList triList) + { + triList.ClearBoolSigAction(136); + triList.ClearBoolSigAction(152); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/INumeric.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/INumeric.cs new file mode 100644 index 00000000..0294a0b5 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/INumeric.cs @@ -0,0 +1,84 @@ +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.SmartObjects; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public interface INumericKeypad + { + void Digit0(bool pressRelease); + void Digit1(bool pressRelease); + void Digit2(bool pressRelease); + void Digit3(bool pressRelease); + void Digit4(bool pressRelease); + void Digit5(bool pressRelease); + void Digit6(bool pressRelease); + void Digit7(bool pressRelease); + void Digit8(bool pressRelease); + void Digit9(bool pressRelease); + + /// + /// Used to hide/show the button and/or text on the left-hand keypad button + /// + bool HasKeypadAccessoryButton1 { get; } + string KeypadAccessoryButton1Label { get; } + void KeypadAccessoryButton1(bool pressRelease); + + bool HasKeypadAccessoryButton2 { get; } + string KeypadAccessoryButton2Label { get; } + void KeypadAccessoryButton2(bool pressRelease); + } + + public interface ISetTopBoxNumericKeypad : INumericKeypad + { + void Dash(bool pressRelease); + void KeypadEnter(bool pressRelease); + } + + /// + /// + /// + public static class INumericExtensions + { + /// + /// Links to the smart object, and sets the misc button's labels on joins x and y + /// + public static void LinkButtons(this INumericKeypad dev, BasicTriList trilist) + { + trilist.SetBoolSigAction(110, dev.Digit0); + trilist.SetBoolSigAction(111, dev.Digit1); + trilist.SetBoolSigAction(112, dev.Digit2); + trilist.SetBoolSigAction(113, dev.Digit3); + trilist.SetBoolSigAction(114, dev.Digit4); + trilist.SetBoolSigAction(115, dev.Digit5); + trilist.SetBoolSigAction(116, dev.Digit6); + trilist.SetBoolSigAction(117, dev.Digit7); + trilist.SetBoolSigAction(118, dev.Digit8); + trilist.SetBoolSigAction(119, dev.Digit9); + trilist.SetBoolSigAction(120, dev.KeypadAccessoryButton1); + trilist.SetBoolSigAction(121, dev.KeypadAccessoryButton2); + trilist.StringInput[111].StringValue = dev.KeypadAccessoryButton1Label; + trilist.StringInput[111].StringValue = dev.KeypadAccessoryButton2Label; + } + + public static void UnlinkButtons(this INumericKeypad dev, BasicTriList trilist) + { + trilist.ClearBoolSigAction(110); + trilist.ClearBoolSigAction(111); + trilist.ClearBoolSigAction(112); + trilist.ClearBoolSigAction(113); + trilist.ClearBoolSigAction(114); + trilist.ClearBoolSigAction(115); + trilist.ClearBoolSigAction(116); + trilist.ClearBoolSigAction(117); + trilist.ClearBoolSigAction(118); + trilist.ClearBoolSigAction(119); + trilist.ClearBoolSigAction(120); + trilist.ClearBoolSigAction(121); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IPower.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IPower.cs new file mode 100644 index 00000000..a392d149 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IPower.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.Fusion; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.SmartObjects; + + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public interface IPower + { + void PowerOn(); + void PowerOff(); + void PowerToggle(); + BoolFeedback PowerIsOnFeedback { get; } + } + + /// + /// + /// + public static class IPowerExtensions + { + public static void LinkButtons(this IPower dev, BasicTriList triList) + { + triList.SetSigFalseAction(101, dev.PowerOn); + triList.SetSigFalseAction(102, dev.PowerOff); + triList.SetSigFalseAction(103, dev.PowerToggle); + dev.PowerIsOnFeedback.LinkInputSig(triList.BooleanInput[101]); + } + + public static void UnlinkButtons(this IPower dev, BasicTriList triList) + { + triList.ClearBoolSigAction(101); + triList.ClearBoolSigAction(102); + triList.ClearBoolSigAction(103); + dev.PowerIsOnFeedback.UnlinkInputSig(triList.BooleanInput[101]); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/ISetTopBoxControls.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/ISetTopBoxControls.cs new file mode 100644 index 00000000..8b043cac --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/ISetTopBoxControls.cs @@ -0,0 +1,55 @@ +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.SmartObjects; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public interface ISetTopBoxControls : IChannel, IColor, IDPad, ISetTopBoxNumericKeypad, + ITransport, IUiDisplayInfo + { + /// + /// Show DVR controls? + /// + bool HasDvr { get; } + + /// + /// Show presets controls? + /// + bool HasPresets { get; } + + /// + /// Show number pad controls? + /// + bool HasNumeric { get; } + + /// + /// Show D-pad controls? + /// + bool HasDpad { get; } + + PepperDash.Essentials.Core.Presets.DevicePresetsModel PresetsModel { get; } + void LoadPresets(string filePath); + + void DvrList(bool pressRelease); + void Replay(bool pressRelease); + } + + public static class ISetTopBoxControlsExtensions + { + public static void LinkButtons(this ISetTopBoxControls dev, BasicTriList triList) + { + triList.SetBoolSigAction(136, dev.DvrList); + triList.SetBoolSigAction(152, dev.Replay); + } + + public static void UnlinkButtons(this ISetTopBoxControls dev, BasicTriList triList) + { + triList.ClearBoolSigAction(136); + triList.ClearBoolSigAction(152); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/ITransport.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/ITransport.cs new file mode 100644 index 00000000..1189c10b --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/ITransport.cs @@ -0,0 +1,53 @@ +using Crestron.SimplSharpPro.DeviceSupport; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public interface ITransport + { + void Play(bool pressRelease); + void Pause(bool pressRelease); + void Rewind(bool pressRelease); + void FFwd(bool pressRelease); + void ChapMinus(bool pressRelease); + void ChapPlus(bool pressRelease); + void Stop(bool pressRelease); + void Record(bool pressRelease); + } + + /// + /// + /// + public static class ITransportExtensions + { + /// + /// Attaches to trilist joins: Play:145, Pause:146, Stop:147, ChapPlus:148, ChapMinus:149, Rewind:150, Ffwd:151, Record:154 + /// + /// + public static void LinkButtons(this ITransport dev, BasicTriList triList) + { + triList.SetBoolSigAction(145, dev.Play); + triList.SetBoolSigAction(146, dev.Pause); + triList.SetBoolSigAction(147, dev.Stop); + triList.SetBoolSigAction(148, dev.ChapPlus); + triList.SetBoolSigAction(149, dev.ChapMinus); + triList.SetBoolSigAction(150, dev.Rewind); + triList.SetBoolSigAction(151, dev.FFwd); + triList.SetBoolSigAction(154, dev.Record); + } + + public static void UnlinkButtons(this ITransport dev, BasicTriList triList) + { + triList.ClearBoolSigAction(145); + triList.ClearBoolSigAction(146); + triList.ClearBoolSigAction(147); + triList.ClearBoolSigAction(148); + triList.ClearBoolSigAction(149); + triList.ClearBoolSigAction(150); + triList.ClearBoolSigAction(151); + triList.ClearBoolSigAction(154); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IUiDisplayInfo.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IUiDisplayInfo.cs new file mode 100644 index 00000000..fb51f7e2 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IUiDisplayInfo.cs @@ -0,0 +1,12 @@ +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// Describes things needed to show on UI + /// + public interface IUiDisplayInfo : IKeyed + { + uint DisplayUiType { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IWarmingCooling.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IWarmingCooling.cs new file mode 100644 index 00000000..2baf1548 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IWarmingCooling.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + /// + /// Defines a class that has warm up and cool down + /// + public interface IWarmingCooling + { + BoolFeedback IsWarmingUpFeedback { get; } + BoolFeedback IsCoolingDownFeedback { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/Template.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/Template.cs new file mode 100644 index 00000000..31f71df9 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/Template.cs @@ -0,0 +1,9 @@ +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.SmartObjects; + +namespace PepperDash.Essentials.Core +{ + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/.IHasFeedbacks_BASE_3692.cs.swp b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/.IHasFeedbacks_BASE_3692.cs.swp new file mode 100644 index 0000000000000000000000000000000000000000..827c466a90c16b427aa5c7398c75833486baef21 GIT binary patch literal 12288 zcmeI2&u<$=6vwANEl=5RKPkd&zodnVEHB znE)ral|zppeq0b7xNw4hfD0$CAOSZHNF0#3ao{_ z1Bc!cSKC^|-j0ylLDcfNQasWD3zb(LKGKSi+`G!-n9C)m+M}LDrZ{|b1D`8zWtpj0 zIB(RMzpb_wFV?DCvoE|f6|f3e1*`&A z0jq#jz$#!B_#Z038ibTj5>k5z!Q=n``QQJ)K2FGw;4|B^5WD4RU(l-pPAn8Uo-+Z6v*Nq2Q&C)A@!y^Mi8g=D8_%O*Xh(>=k$E`~uz0-WC(N@f#=g zFz~5DlDKt6(4b36CmtHFi@Z?lHbmQhw0c;#>rj%85e)thy7xn-4*IC>Y96Todja+n zlvG5t8Yq1}pMQboPm1=<9A#=Y01q|pyr+^*hsoZ;pdpC-Flk^npezij!&)uLTUa}X zHnM}^?qJ}$$)v+|TQsO@o57OnL`Ia+5R+5UOg&^3mx}sZ+}DZ<)1P@VJ3t!3D1uS0 zL8Q5CGM^t5YmTm5;yvCVeG*|~Qn48^I?C>j&az``l$?-6nN;?TFwNz>y&^f@MG?8R ypc99+Hk0vHDctM`d@YG%A$57+qt;WJcihz=di$29iU>m_XY|MZ7I7K)W%3^|8&Y!s literal 0 HcmV?d00001 diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/.IHasFeedbacks_LOCAL_3692.cs.swp b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/.IHasFeedbacks_LOCAL_3692.cs.swp new file mode 100644 index 0000000000000000000000000000000000000000..e06196a43c167ca2db134d29e0eaaa0b14ce9971 GIT binary patch literal 12288 zcmeI2!EYNy6vn4LPzHB3A+$U9T=wY2_+c zFA348@%py9xw>9iSiO1i`AgGoT^$*W?X(V@I0w$Mi_=q+ChcdO3-r{JubnstY{J$7 z>wtB@I$#~J4p;}Q1J;3)?0}9=vG-uW>46CggY&5a=fOvNvJO}WtOM2o>wtB@I$#~J z4p;}Q1J(iSfOX)1=m2jpwsVHDPaj6``2TUym{NC%6y(0AGTS!AD>V%zzSj8vOkz zV?To*z!%_sum%V`1b%#kv2Va_umN5Kr@`+z#=Zt0fCzYC2HZ!^zkz$;C-6DA3+@2p z$0xuZ)&c8)b-+4c9k34kXAazAIs7H$a!aBXx7m^X<-fP45=hKgXXlNoIz_U-o)-)UT;&r%O#;i0%~Tva0|)B z<7e%S;6|`8oVLTuvWc7MC)s6!bYYGHOxHv>x!Iv*K@K)pIci@u7ij_##L*LxP3j+$9lnK+0 zZxTkb78$>yxaNCxQh3BTbwjT~OK~R}nz9*Y%cpK*Ht*duJzPyq8Ji3al#3~*{ql96 ztNj|wyIKSaxtKlISmhAWs;Bhj?EdqVy;IaT_b8KDmP@ega!JLV4wt?8L4z06{kVZO znz-+i!&@yWTBuxy8tImAcQA0%WYQ739m3(Z87zg4WdMVH+}zk|8rPGr1qy3#!)`_J zn-7%CqMU>60$+`zD3m%ITkR<=I_|0$ymO$b PGORd-0t$rm>SgvXNT_AJ literal 0 HcmV?d00001 diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/.IHasFeedbacks_REMOTE_3692.cs.swp b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/.IHasFeedbacks_REMOTE_3692.cs.swp new file mode 100644 index 0000000000000000000000000000000000000000..842256ccbf7ddc69de45a8dc3f387833fe54012c GIT binary patch literal 12288 zcmeI2&2Jk;7{;d{0R~D-IRGc73lg?)cJonDh?@YhT@p2ktJ;k~3Pj`e*j}>UU1oNj zSjNB!ZspJ;h>r_`0~b#44{+hc6(qP+>H)z4i5mx=+4b58=N)XH0P&ptnE+Vaq7bfZ?_xG8Xw%+1bBt64v3oTZPPxq93L(DT&_ zXa%$aS^=$qRzNGD70?PCYX!pK1bGJv+%;68G^|e?sE1#5r4`T$Xa%$aS^=$qRzNGD z70?Q31+)TM0j;FCkkN^ME-~T_KBIJAU3HTVi58eX|Y=QIO9C!-+ z^bjFmgU`SKyajq-7wmvbU>W>+l8~F=D{viL12r%Qrof}%uLlYF6MP3g0Iz|k!5;OE1(t73TOrXuL=wZMH!H|>c^i!RCgVl%8-SqY?nz%_nx}D7)i(X z%DW-=)CEr6WdHO4l1KOV%^k)Y~9+ofju+|HBHkXehKp;?MVzI2l+LVc9sn5FLY`Z|z z<)o5yvMd{C8!4~Ea;ON*O4#ONo=mb)_YymGbz1|dj*x;bP$T9U%(WV;y20Ft-zvT& zI_MpTmKfgA*jBvo2!X-sjz+cQ`|c6KQf7z6mAr0hi4GRak~>alw<(@>C4DY=KM!_G zzaMFZ)jLO>X0}NkNt2~b&(-Eg>UmOqaI{;m*}kFB{b=Ld#ws{XD5mD4`i@6YNhQ-y z-fY#S#lvDG1kNB=k>t3{B~3}XN#7SE&P+qICe9`^wH{q2C`RyTGd+Wx82@|T+w?ZQ zmqv+iWR|ABIIKAC)FC~)Ks_Y9*l|4GsAXZ8PR(>~(yWLE7fZeDE?ropyUEj{B3FL* zXY#J^G-xU61S8{`pC;Q1O{6(5x zN!nLq6tEFxG8S^&1xrSq4imk_;SI;L-Kc@xfHK#m25YqhZ(;2W+DHzDo5PMZHIohx z+kS(}w(2Z+7zvLu>SA!}H)9V;#-)D!4Q_{$`l>zgq;`NfgmDDpxjJ6RMU&ZlzgY8h zh|%ENJ@x literal 0 HcmV?d00001 diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CodecInterfaces.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CodecInterfaces.cs new file mode 100644 index 00000000..afb1da17 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CodecInterfaces.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + /// + /// Adds control of codec receive volume + /// + public interface IReceiveVolume + { + // Break this out into 3 interfaces + void SetReceiveVolume(ushort level); + void ReceiveMuteOn(); + void ReceiveMuteOff(); + void ReceiveMuteToggle(); + IntFeedback ReceiveLevelFeedback { get; } + BoolFeedback ReceiveMuteIsOnFeedback { get; } + } + + /// + /// Adds control of codec transmit volume + /// + public interface ITransmitVolume + { + void SetTransmitVolume(ushort level); + void TransmitMuteOn(); + void TransmitMuteOff(); + void TransmitMuteToggle(); + IntFeedback TransmitLevelFeedback { get; } + BoolFeedback TransmitMuteIsOnFeedback { get; } + } + + /// + /// Adds control of codec privacy function (microphone mute) + /// + public interface IPrivacy + { + void PrivacyModeOn(); + void PrivacyModeOff(); + void PrivacyModeToggle(); + BoolFeedback PrivacyModeIsOnFeedback { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CodecInterfaces.cs.orig b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CodecInterfaces.cs.orig new file mode 100644 index 00000000..04d5aac9 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CodecInterfaces.cs.orig @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + /// + /// Requirements for a device that has dialing capabilities + /// + public interface IHasDialer + { + // Add requirements for Dialer functionality + + void Dial(string number); +<<<<<<< HEAD + void EndCall(string number); +======= + void EndCall(object activeCall); + void EndAllCalls(); +>>>>>>> origin/feature/cisco-spark-2 + void AcceptCall(); + void RejectCall(); + void SendDtmf(string digit); + + IntFeedback ActiveCallCountFeedback { get; } + BoolFeedback IncomingCallFeedback { get; } + } + + /// + /// Defines minimum volume controls for a codec device with dialing capabilities + /// + public interface ICodecAudio : IBasicVolumeWithFeedback, IPrivacy + { + + } + + /// + /// Adds control of codec receive volume + /// + public interface IReceiveVolume + { + // Break this out into 3 interfaces + void SetReceiveVolume(ushort level); + void ReceiveMuteOn(); + void ReceiveMuteOff(); + void ReceiveMuteToggle(); + IntFeedback ReceiveLevelFeedback { get; } + BoolFeedback ReceiveMuteIsOnFeedback { get; } + } + + /// + /// Adds control of codec transmit volume + /// + public interface ITransmitVolume + { + void SetTransmitVolume(ushort level); + void TransmitMuteOn(); + void TransmitMuteOff(); + void TransmitMuteToggle(); + IntFeedback TransmitLevelFeedback { get; } + BoolFeedback TransmitMuteIsOnFeedback { get; } + } + + /// + /// Adds control of codec privacy function (microphone mute) + /// + public interface IPrivacy + { + void PrivacyModeOn(); + void PrivacyModeOff(); + void PrivacyModeToggle(); + BoolFeedback PrivacyModeIsOnFeedback { get; } + } + + public interface IHasCallHistory + { + // Add recent calls list + } + + public interface IHasDirectory + { + + } + + public interface IHasObtp + { + + // Upcoming Meeting warning event + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CrestronProcessor.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CrestronProcessor.cs new file mode 100644 index 00000000..1b0264eb --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/CrestronProcessor.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core.CrestronIO; + +namespace PepperDash.Essentials.Core.Devices +{ + /// + /// This wrapper class is meant to allow interfaces to be applied to any Crestron processor + /// + public class CrestronProcessor : Device, ISwitchedOutputCollection + { + public Dictionary SwitchedOutputs { get; private set; } + + public Crestron.SimplSharpPro.CrestronControlSystem Processor { get; private set; } + + public CrestronProcessor(string key) + : base(key) + { + SwitchedOutputs = new Dictionary(); + Processor = Global.ControlSystem; + + GetRelays(); + } + + /// + /// Creates a GenericRelayDevice for each relay on the processor and adds them to the SwitchedOutputs collection + /// + void GetRelays() + { + try + { + if (Processor.SupportsRelay) + { + for (uint i = 1; i <= Processor.NumberOfRelayPorts; i++) + { + var relay = new GenericRelayDevice(string.Format("{0}-relay-{1}", this.Key, i), Processor.RelayPorts[i]); + SwitchedOutputs.Add(i, relay); + } + } + } + catch (Exception e) + { + Debug.Console(1, this, "Error Getting Relays from processor:\n '{0}'", e); + } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceApiBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceApiBase.cs new file mode 100644 index 00000000..34669ff5 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceApiBase.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core.Devices +{ + /// + /// Base class for all Device APIs + /// + public abstract class DeviceApiBase + { + public Dictionary ActionApi { get; protected set; } + public Dictionary FeedbackApi { get; protected set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceJsonApi.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceJsonApi.cs new file mode 100644 index 00000000..32d53388 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceJsonApi.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.Reflection; +using Newtonsoft.Json; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + public class DeviceJsonApi + { + /// + /// + /// + /// + public static void DoDeviceActionWithJson(string json) + { + var action = JsonConvert.DeserializeObject(json); + DoDeviceAction(action); + } + + + /// + /// + /// + /// + public static void DoDeviceAction(DeviceActionWrapper action) + { + var key = action.DeviceKey; + var obj = FindObjectOnPath(key); + if (obj == null) + return; + + CType t = obj.GetType(); + var method = t.GetMethod(action.MethodName); + if (method == null) + { + Debug.Console(0, "Method '{0}' not found", action.MethodName); + return; + } + var mParams = method.GetParameters(); + // Add empty params if not provided + if (action.Params == null) action.Params = new object[0]; + if (mParams.Length > action.Params.Length) + { + Debug.Console(0, "Method '{0}' requires {1} params", action.MethodName, mParams.Length); + return; + } + object[] convertedParams = mParams + .Select((p, i) => Convert.ChangeType(action.Params[i], p.ParameterType, + System.Globalization.CultureInfo.InvariantCulture)) + .ToArray(); + object ret = method.Invoke(obj, convertedParams); + } + + /// + /// + /// + /// + /// + public static string GetProperties(string deviceObjectPath) + { + var obj = FindObjectOnPath(deviceObjectPath); + if (obj == null) + return "{ \"error\":\"No Device\"}"; + + CType t = obj.GetType(); + // get the properties and set them into a new collection of NameType wrappers + var props = t.GetProperties().Select(p => new PropertyNameType(p, obj)); + return JsonConvert.SerializeObject(props, Formatting.Indented); + } + + /// + /// + /// + /// + /// + public static string GetMethods(string deviceObjectPath) + { + var obj = FindObjectOnPath(deviceObjectPath); + if (obj == null) + return "{ \"error\":\"No Device\"}"; + + // Package up method names using helper objects + CType t = obj.GetType(); + var methods = t.GetMethods() + .Where(m => !m.IsSpecialName) + .Select(p => new MethodNameParams(p)); + return JsonConvert.SerializeObject(methods, Formatting.Indented); + } + + public static string GetApiMethods(string deviceObjectPath) + { + var obj = FindObjectOnPath(deviceObjectPath); + if (obj == null) + return "{ \"error\":\"No Device\"}"; + + // Package up method names using helper objects + CType t = obj.GetType(); + var methods = t.GetMethods() + .Where(m => !m.IsSpecialName) + .Where(m => m.GetCustomAttributes(typeof(ApiAttribute), true).Any()) + .Select(p => new MethodNameParams(p)); + return JsonConvert.SerializeObject(methods, Formatting.Indented); + } + + + /// + /// Walks down a dotted object path, starting with a Device, and returns the object + /// at the end of the path + /// + public static object FindObjectOnPath(string deviceObjectPath) + { + var path = deviceObjectPath.Split('.'); + + var dev = DeviceManager.GetDeviceForKey(path[0]); + if (dev == null) + { + Debug.Console(0, "Device {0} not found", path[0]); + return null; + } + + // loop through any dotted properties + object obj = dev; + if (path.Length > 1) + { + for (int i = 1; i < path.Length; i++) + { + var objName = path[i]; + string indexStr = null; + var indexOpen = objName.IndexOf('['); + if (indexOpen != -1) + { + var indexClose = objName.IndexOf(']'); + if (indexClose == -1) + { + Debug.Console(0, dev, "ERROR Unmatched index brackets"); + return null; + } + // Get the index and strip quotes if any + indexStr = objName.Substring(indexOpen + 1, indexClose - indexOpen - 1).Replace("\"", ""); + objName = objName.Substring(0, indexOpen); + Debug.Console(0, dev, " Checking for collection '{0}', index '{1}'", objName, indexStr); + } + + CType oType = obj.GetType(); + var prop = oType.GetProperty(objName); + if (prop == null) + { + Debug.Console(0, dev, "Property {0} not found on {1}", objName, path[i - 1]); + return null; + } + // if there's an index, try to get the property + if (indexStr != null) + { + if (!typeof(ICollection).IsAssignableFrom(prop.PropertyType)) + { + Debug.Console(0, dev, "Property {0} is not collection", objName); + return null; + } + var collection = prop.GetValue(obj, null) as ICollection; + // Get the indexed items "property" + var indexedPropInfo = prop.PropertyType.GetProperty("Item"); + // These are the parameters for the indexing. Only care about one + var indexParams = indexedPropInfo.GetIndexParameters(); + if (indexParams.Length > 0) + { + Debug.Console(0, " Indexed, param type: {0}", indexParams[0].ParameterType.Name); + var properParam = Convert.ChangeType(indexStr, indexParams[0].ParameterType, + System.Globalization.CultureInfo.InvariantCulture); + try + { + obj = indexedPropInfo.GetValue(collection, new object[] { properParam }); + } + // if the index is bad, catch it here. + catch (Crestron.SimplSharp.Reflection.TargetInvocationException e) + { + if (e.InnerException is ArgumentOutOfRangeException) + Debug.Console(0, " Index Out of range"); + else if (e.InnerException is KeyNotFoundException) + Debug.Console(0, " Key not found"); + return null; + } + } + + } + else + obj = prop.GetValue(obj, null); + } + } + return obj; + } + + /// + /// Sets a property on an object. + /// + /// + /// + public static string SetProperty(string deviceObjectPath) + { + throw new NotImplementedException("This could be really useful. Finish it please"); + + //var obj = FindObjectOnPath(deviceObjectPath); + //if (obj == null) + // return "{\"error\":\"No object found\"}"; + + //CType t = obj.GetType(); + + + //// get the properties and set them into a new collection of NameType wrappers + //var props = t.GetProperties().Select(p => new PropertyNameType(p, obj)); + //return JsonConvert.SerializeObject(props, Formatting.Indented); + } + } + + public class DeviceActionWrapper + { + public string DeviceKey { get; set; } + public string MethodName { get; set; } + public object[] Params { get; set; } + } + + public class PropertyNameType + { + object Parent; + + [JsonIgnore] + public PropertyInfo PropInfo { get; private set; } + public string Name { get { return PropInfo.Name; } } + public string Type { get { return PropInfo.PropertyType.Name; } } + public string Value { get + { + if (PropInfo.CanRead) + { + try + { + return PropInfo.GetValue(Parent, null).ToString(); + } + catch (Exception) + { + return null; + } + } + else + return null; + } } + + public bool CanRead { get { return PropInfo.CanRead; } } + public bool CanWrite { get { return PropInfo.CanWrite; } } + + + public PropertyNameType(PropertyInfo info, object parent) + { + PropInfo = info; + Parent = parent; + } + } + + public class MethodNameParams + { + [JsonIgnore] + public MethodInfo MethodInfo { get; private set; } + + public string Name { get { return MethodInfo.Name; } } + public IEnumerable Params { get { + return MethodInfo.GetParameters().Select(p => + new NameType { Name = p.Name, Type = p.ParameterType.Name }); + } } + + public MethodNameParams(MethodInfo info) + { + MethodInfo = info; + } + } + + public class NameType + { + public string Name { get; set; } + public string Type { get; set; } + } + + [AttributeUsage(AttributeTargets.All)] + public class ApiAttribute : CAttribute + { + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceManager.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceManager.cs new file mode 100644 index 00000000..6fa1f558 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceManager.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.EthernetCommunication; +using Crestron.SimplSharpPro.UI; +using Crestron.SimplSharp.Reflection; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + public static class DeviceManager + { + //public static List Devices { get { return _Devices; } } + //static List _Devices = new List(); + + static Dictionary Devices = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Returns a copy of all the devices in a list + /// + public static List AllDevices { get { return new List(Devices.Values); } } + + public static void Initialize(CrestronControlSystem cs) + { + CrestronConsole.AddNewConsoleCommand(ListDeviceCommStatuses, "devcommstatus", "Lists the communication status of all devices", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ListDeviceFeedbacks, "devfb", "Lists current feedbacks", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ListDevices, "devlist", "Lists current managed devices", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(DeviceJsonApi.DoDeviceActionWithJson, "devjson", "", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => + { + CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetProperties(s)); + }, "devprops", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => + { + CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetMethods(s)); + }, "devmethods", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => + { + CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetApiMethods(s)); + }, "apimethods", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(SimulateComReceiveOnDevice, "devsimreceive", + "Simulates incoming data on a com device", ConsoleAccessLevelEnum.AccessOperator); + } + + /// + /// Calls activate steps on all Device class items + /// + public static void ActivateAll() + { + // PreActivate all devices + foreach (var d in Devices.Values) + { + try + { + if (d is Device) + (d as Device).PreActivate(); + } + catch (Exception e) + { + Debug.Console(0, d, "ERROR: Device PreActivation failure:\r{0}", e); + } + } + + // Activate all devices + foreach (var d in Devices.Values) + { + try + { + if (d is Device) + (d as Device).Activate(); + } + catch (Exception e) + { + Debug.Console(0, d, "ERROR: Device Activation failure:\r{0}", e); + } + } + + // PostActivate all devices + foreach (var d in Devices.Values) + { + try + { + if (d is Device) + (d as Device).PostActivate(); + } + catch (Exception e) + { + Debug.Console(0, d, "ERROR: Device PostActivation failure:\r{0}", e); + } + } + } + + /// + /// Calls activate on all Device class items + /// + public static void DeactivateAll() + { + foreach (var d in Devices.Values) + { + if (d is Device) + (d as Device).Deactivate(); + } + } + + //static void ListMethods(string devKey) + //{ + // var dev = GetDeviceForKey(devKey); + // if(dev != null) + // { + // var type = dev.GetType().GetCType(); + // var methods = type.GetMethods(BindingFlags.Public|BindingFlags.Instance); + // var sb = new StringBuilder(); + // sb.AppendLine(string.Format("{2} methods on [{0}] ({1}):", dev.Key, type.Name, methods.Length)); + // foreach (var m in methods) + // { + // sb.Append(string.Format("{0}(", m.Name)); + // var pars = m.GetParameters(); + // foreach (var p in pars) + // sb.Append(string.Format("({1}){0} ", p.Name, p.ParameterType.Name)); + // sb.AppendLine(")"); + // } + // CrestronConsole.ConsoleCommandResponse(sb.ToString()); + // } + //} + + static void ListDevices(string s) + { + Debug.Console(0, "{0} Devices registered with Device Mangager:",Devices.Count); + var sorted = Devices.Values.ToList(); + sorted.Sort((a, b) => a.Key.CompareTo(b.Key)); + + foreach (var d in sorted) + { + var name = d is IKeyName ? (d as IKeyName).Name : "---"; + Debug.Console(0, " [{0}] {1}", d.Key, name); + } + } + + static void ListDeviceFeedbacks(string devKey) + { + var dev = GetDeviceForKey(devKey); + if(dev == null) + { + Debug.Console(0, "Device '{0}' not found", devKey); + return; + } + var statusDev = dev as IHasFeedback; + if(statusDev == null) + { + Debug.Console(0, "Device '{0}' does not have visible feedbacks", devKey); + return; + } + statusDev.DumpFeedbacksToConsole(true); + } + + //static void ListDeviceCommands(string devKey) + //{ + // var dev = GetDeviceForKey(devKey); + // if (dev == null) + // { + // Debug.Console(0, "Device '{0}' not found", devKey); + // return; + // } + // Debug.Console(0, "This needs to be reworked. Stay tuned.", devKey); + //} + + static void ListDeviceCommStatuses(string input) + { + var sb = new StringBuilder(); + foreach (var dev in Devices.Values) + { + if (dev is ICommunicationMonitor) + sb.Append(string.Format("{0}: {1}\r", dev.Key, (dev as ICommunicationMonitor).CommunicationMonitor.Status)); + } + CrestronConsole.ConsoleCommandResponse(sb.ToString()); + } + + + //static void DoDeviceCommand(string command) + //{ + // Debug.Console(0, "Not yet implemented. Stay tuned"); + //} + + public static void AddDevice(IKeyed newDev) + { + // Check for device with same key + //var existingDevice = _Devices.FirstOrDefault(d => d.Key.Equals(newDev.Key, StringComparison.OrdinalIgnoreCase)); + ////// If it exists, remove or warn?? + //if (existingDevice != null) + if(Devices.ContainsKey(newDev.Key)) + { + Debug.Console(0, newDev, "WARNING: A device with this key already exists. Not added to manager"); + return; + } + Devices.Add(newDev.Key, newDev); + //if (!(_Devices.Contains(newDev))) + // _Devices.Add(newDev); + } + + public static void RemoveDevice(IKeyed newDev) + { + if(newDev == null) + return; + if (Devices.ContainsKey(newDev.Key)) + Devices.Remove(newDev.Key); + //if (_Devices.Contains(newDev)) + // _Devices.Remove(newDev); + else + Debug.Console(0, "Device manager: Device '{0}' does not exist in manager. Cannot remove", newDev.Key); + } + + public static IEnumerable GetDeviceKeys() + { + //return _Devices.Select(d => d.Key).ToList(); + return Devices.Keys; + } + + public static IEnumerable GetDevices() + { + //return _Devices.Select(d => d.Key).ToList(); + return Devices.Values; + } + + public static IKeyed GetDeviceForKey(string key) + { + //return _Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (key != null && Devices.ContainsKey(key)) + return Devices[key]; + + return null; + } + + /// + /// Console handler that simulates com port data receive + /// + /// + public static void SimulateComReceiveOnDevice(string s) + { + // devcomsim:1 xyzabc + var match = Regex.Match(s, @"(\S*)\s*(.*)"); + if (match.Groups.Count < 3) + { + CrestronConsole.ConsoleCommandResponse(" Format: devsimreceive:P "); + return; + } + //Debug.Console(2, "**** {0} - {1} ****", match.Groups[1].Value, match.Groups[2].Value); + + ComPortController com = GetDeviceForKey(match.Groups[1].Value) as ComPortController; + if (com == null) + { + CrestronConsole.ConsoleCommandResponse("'{0}' is not a comm port device", match.Groups[1].Value); + return; + } + com.SimulateReceive(match.Groups[2].Value); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceManager.cs.orig b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceManager.cs.orig new file mode 100644 index 00000000..e768807b --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DeviceManager.cs.orig @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.EthernetCommunication; +using Crestron.SimplSharpPro.UI; +using Crestron.SimplSharp.Reflection; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + public static class DeviceManager + { + //public static List Devices { get { return _Devices; } } + //static List _Devices = new List(); + + static Dictionary Devices = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Returns a copy of all the devices in a list + /// + public static List AllDevices { get { return new List(Devices.Values); } } + + public static void Initialize(CrestronControlSystem cs) + { + CrestronConsole.AddNewConsoleCommand(ListDeviceCommStatuses, "devcommstatus", "Lists the communication status of all devices", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ListDeviceFeedbacks, "devfb", "Lists current feedbacks", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ListDevices, "devlist", "Lists current managed devices", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(DeviceJsonApi.DoDeviceActionWithJson, "devjson", "", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => + { + CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetProperties(s)); + }, "devprops", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => + { + CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetMethods(s)); + }, "devmethods", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => + { + CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetApiMethods(s)); + }, "apimethods", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(SimulateComReceiveOnDevice, "devsimreceive", + "Simulates incoming data on a com device", ConsoleAccessLevelEnum.AccessOperator); + } + + /// + /// Calls activate on all Device class items + /// + public static void ActivateAll() + { + foreach (var d in Devices.Values) +<<<<<<< HEAD + { + try + { + if (d is Device) + (d as Device).Activate(); + } + catch (Exception e) + { + Debug.Console(0, d, "ERROR: Device activation failure:\r{0}", e); + } + } +======= + { + try + { + if (d is Device) + (d as Device).Activate(); + } + catch (Exception e) + { + Debug.Console(0, d, "ERROR: Device activation failure:\r{0}", e); + } + } +>>>>>>> origin/feature/ecs-342-neil + } + + /// + /// Calls activate on all Device class items + /// + public static void DeactivateAll() + { + foreach (var d in Devices.Values) + { + if (d is Device) + (d as Device).Deactivate(); + } + } + + //static void ListMethods(string devKey) + //{ + // var dev = GetDeviceForKey(devKey); + // if(dev != null) + // { + // var type = dev.GetType().GetCType(); + // var methods = type.GetMethods(BindingFlags.Public|BindingFlags.Instance); + // var sb = new StringBuilder(); + // sb.AppendLine(string.Format("{2} methods on [{0}] ({1}):", dev.Key, type.Name, methods.Length)); + // foreach (var m in methods) + // { + // sb.Append(string.Format("{0}(", m.Name)); + // var pars = m.GetParameters(); + // foreach (var p in pars) + // sb.Append(string.Format("({1}){0} ", p.Name, p.ParameterType.Name)); + // sb.AppendLine(")"); + // } + // CrestronConsole.ConsoleCommandResponse(sb.ToString()); + // } + //} + + static void ListDevices(string s) + { + Debug.Console(0, "{0} Devices registered with Device Mangager:",Devices.Count); + var sorted = Devices.Values.ToList(); + sorted.Sort((a, b) => a.Key.CompareTo(b.Key)); + + foreach (var d in sorted) + { + var name = d is IKeyName ? (d as IKeyName).Name : "---"; + Debug.Console(0, " [{0}] {1}", d.Key, name); + } + } + + static void ListDeviceFeedbacks(string devKey) + { + var dev = GetDeviceForKey(devKey); + if(dev == null) + { + Debug.Console(0, "Device '{0}' not found", devKey); + return; + } + var statusDev = dev as IHasFeedback; + if(statusDev == null) + { + Debug.Console(0, "Device '{0}' does not have visible feedbacks", devKey); + return; + } + statusDev.DumpFeedbacksToConsole(true); + } + + //static void ListDeviceCommands(string devKey) + //{ + // var dev = GetDeviceForKey(devKey); + // if (dev == null) + // { + // Debug.Console(0, "Device '{0}' not found", devKey); + // return; + // } + // Debug.Console(0, "This needs to be reworked. Stay tuned.", devKey); + //} + + static void ListDeviceCommStatuses(string input) + { + var sb = new StringBuilder(); + foreach (var dev in Devices.Values) + { + if (dev is ICommunicationMonitor) + sb.Append(string.Format("{0}: {1}\r", dev.Key, (dev as ICommunicationMonitor).CommunicationMonitor.Status)); + } + CrestronConsole.ConsoleCommandResponse(sb.ToString()); + } + + + //static void DoDeviceCommand(string command) + //{ + // Debug.Console(0, "Not yet implemented. Stay tuned"); + //} + + public static void AddDevice(IKeyed newDev) + { + // Check for device with same key + //var existingDevice = _Devices.FirstOrDefault(d => d.Key.Equals(newDev.Key, StringComparison.OrdinalIgnoreCase)); + ////// If it exists, remove or warn?? + //if (existingDevice != null) + if(Devices.ContainsKey(newDev.Key)) + { + Debug.Console(0, newDev, "WARNING: A device with this key already exists. Not added to manager"); + return; + } + Devices.Add(newDev.Key, newDev); + //if (!(_Devices.Contains(newDev))) + // _Devices.Add(newDev); + } + + public static void RemoveDevice(IKeyed newDev) + { + if(newDev == null) + return; + if (Devices.ContainsKey(newDev.Key)) + Devices.Remove(newDev.Key); + //if (_Devices.Contains(newDev)) + // _Devices.Remove(newDev); + else + Debug.Console(0, "Device manager: Device '{0}' does not exist in manager. Cannot remove", newDev.Key); + } + + public static IEnumerable GetDeviceKeys() + { + //return _Devices.Select(d => d.Key).ToList(); + return Devices.Keys; + } + + public static IEnumerable GetDevices() + { + //return _Devices.Select(d => d.Key).ToList(); + return Devices.Values; + } + + public static IKeyed GetDeviceForKey(string key) + { + //return _Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (key != null && Devices.ContainsKey(key)) + return Devices[key]; + + return null; + } + + /// + /// Console handler that simulates com port data receive + /// + /// + public static void SimulateComReceiveOnDevice(string s) + { + // devcomsim:1 xyzabc + var match = Regex.Match(s, @"(\S*)\s*(.*)"); + if (match.Groups.Count < 3) + { + CrestronConsole.ConsoleCommandResponse(" Format: devsimreceive:P "); + return; + } + //Debug.Console(2, "**** {0} - {1} ****", match.Groups[1].Value, match.Groups[2].Value); + + ComPortController com = GetDeviceForKey(match.Groups[1].Value) as ComPortController; + if (com == null) + { + CrestronConsole.ConsoleCommandResponse("'{0}' is not a comm port device", match.Groups[1].Value); + return; + } + com.SimulateReceive(match.Groups[2].Value); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DisplayUiConstants.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DisplayUiConstants.cs new file mode 100644 index 00000000..a42239fc --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/DisplayUiConstants.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + /// + /// Integers that represent the "source type number" for given sources. + /// Primarily used by the UI to calculate subpage join offsets + /// Note, for UI, only values 1-49 are valid. + /// + public class DisplayUiConstants + { + public const uint TypeRadio = 1; + public const uint TypeDirecTv = 9; + public const uint TypeBluray = 13; + public const uint TypeChromeTv = 15; + public const uint TypeFireTv = 16; + public const uint TypeAppleTv = 17; + public const uint TypeRoku = 18; + public const uint TypeLaptop = 31; + public const uint TypePc = 32; + + public const uint TypeNoControls = 49; + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/FIND HOMES Interfaces.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/FIND HOMES Interfaces.cs new file mode 100644 index 00000000..318f5179 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/FIND HOMES Interfaces.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + public interface IOnline + { + BoolFeedback IsOnline { get; } + } + + ///// + ///// ** WANT THIS AND ALL ITS FRIENDS TO GO AWAY ** + ///// Defines a class that has a list of CueAction objects, typically + ///// for linking functions to user interfaces or API calls + ///// + //public interface IHasCueActionList + //{ + // List CueActionList { get; } + //} + + + //public interface IHasComPortsHardware + //{ + // IComPorts ComPortsDevice { get; } + //} + + /// + /// Describes a device that can have a video sync providing device attached to it + /// + public interface IAttachVideoStatus : IKeyed + { + // Extension methods will depend on this + } + + /// + /// For display classes that can provide usage data + /// + public interface IDisplayUsage + { + IntFeedback LampHours { get; } + } + + public interface IMakeModel : IKeyed + { + string DeviceMake { get; } + string DeviceModel { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/GenericMonitoredTcpDevice.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/GenericMonitoredTcpDevice.cs new file mode 100644 index 00000000..e7267782 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/GenericMonitoredTcpDevice.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core.Devices +{ + public class GenericCommunicationMonitoredDevice : Device, ICommunicationMonitor + { + IBasicCommunication Client; + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + public GenericCommunicationMonitoredDevice(string key, string name, IBasicCommunication comm, string pollString, + long pollTime, long warningTime, long errorTime) + : base(key, name) + { + Client = comm; + CommunicationMonitor = new GenericCommunicationMonitor(this, Client, pollTime, warningTime, errorTime, pollString); + + // ------------------------------------------------------DELETE THIS + CommunicationMonitor.StatusChange += (o, a) => + { + Debug.Console(2, this, "Communication monitor status change: {0}", a.Status); + }; + + } + + public GenericCommunicationMonitoredDevice(string key, string name, IBasicCommunication comm, string pollString) + : this(key, name, comm, pollString, 30000, 120000, 300000) + { + } + + + /// + /// Generic monitor for TCP reachability. Default with 30s poll, 120s warning and 300s error times + /// + [Obsolete] + public GenericCommunicationMonitoredDevice(string key, string name, string hostname, int port, string pollString) + : this(key, name, hostname, port, pollString, 30000, 120000, 300000) + { + } + + /// + /// Monitor for TCP reachability + /// + [Obsolete] + public GenericCommunicationMonitoredDevice(string key, string name, string hostname, int port, string pollString, + long pollTime, long warningTime, long errorTime) + : base(key, name) + { + Client = new GenericTcpIpClient(key + "-tcp", hostname, port, 512); + CommunicationMonitor = new GenericCommunicationMonitor(this, Client, pollTime, warningTime, errorTime, pollString); + + // ------------------------------------------------------DELETE THIS + CommunicationMonitor.StatusChange += (o, a) => + { + Debug.Console(2, this, "Communication monitor status change: {0}", a.Status); + }; + } + + + public override bool CustomActivate() + { + CommunicationMonitor.Start(); + return true; + } + + public override bool Deactivate() + { + CommunicationMonitor.Stop(); + return true; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IAttachVideoStatusExtensions.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IAttachVideoStatusExtensions.cs new file mode 100644 index 00000000..e5218ee0 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IAttachVideoStatusExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + public static class IAttachVideoStatusExtensions + { + /// + /// Gets the VideoStatusOutputs for the device + /// + /// + /// Attached VideoStatusOutputs or the default if none attached + public static VideoStatusOutputs GetVideoStatuses(this IAttachVideoStatus attachedDev) + { + // See if this device is connected to a status-providing port + var tl = TieLineCollection.Default.FirstOrDefault(t => + t.SourcePort.ParentDevice == attachedDev + && t.DestinationPort is RoutingInputPortWithVideoStatuses); + if (tl != null) + { + // if so, and it's got status, return it -- or null + var port = tl.DestinationPort as RoutingInputPortWithVideoStatuses; + if (port != null) + return port.VideoStatus; + } + return VideoStatusOutputs.NoStatus; + } + + public static bool HasVideoStatuses(this IAttachVideoStatus attachedDev) + { + return TieLineCollection.Default.FirstOrDefault(t => + t.SourcePort.ParentDevice == attachedDev + && t.DestinationPort is RoutingInputPortWithVideoStatuses) != null; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks.cs new file mode 100644 index 00000000..a21d2a3a --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharp.Reflection; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public interface IHasFeedback : IKeyed + { + /// + /// This method shall return a list of all Output objects on a device, + /// including all "aggregate" devices. + /// + FeedbackCollection Feedbacks { get; } + + } + + + public static class IHasFeedbackExtensions + { + public static void DumpFeedbacksToConsole(this IHasFeedback source, bool getCurrentStates) + { + CType t = source.GetType(); + // get the properties and set them into a new collection of NameType wrappers + var props = t.GetProperties().Select(p => new PropertyNameType(p, t)); + + + + var feedbacks = source.Feedbacks.OrderBy(x => x.Type); + if (feedbacks != null) + { + Debug.Console(0, source, "\n\nAvailable feedbacks:"); + foreach (var f in feedbacks) + { + string val = ""; + string type = ""; + if (getCurrentStates) + { + if (f is BoolFeedback) + { + val = f.BoolValue.ToString(); + type = "boolean"; + } + else if (f is IntFeedback) + { + val = f.IntValue.ToString(); + type = "integer"; + } + else if (f is StringFeedback) + { + val = f.StringValue; + type = "string"; + } + } + Debug.Console(0, "{0,-12} {1, -25} {2}", type, + (string.IsNullOrEmpty(f.Key) ? "-no key-" : f.Key), val); + } + } + else + Debug.Console(0, source, "No available outputs:"); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_BACKUP_3692.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_BACKUP_3692.cs new file mode 100644 index 00000000..8af64e92 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_BACKUP_3692.cs @@ -0,0 +1,135 @@ +<<<<<<< HEAD +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharp.Reflection; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public interface IHasFeedback : IKeyed + { + /// + /// This method shall return a list of all Output objects on a device, + /// including all "aggregate" devices. + /// + List Feedbacks { get; } + + } + + + public static class IHasFeedbackExtensions + { + public static void DumpFeedbacksToConsole(this IHasFeedback source, bool getCurrentStates) + { + CType t = source.GetType(); + // get the properties and set them into a new collection of NameType wrappers + var props = t.GetProperties().Select(p => new PropertyNameType(p, t)); + + + + var feedbacks = source.Feedbacks.OrderBy(x => x.Type); + if (feedbacks != null) + { + Debug.Console(0, source, "\n\nAvailable feedbacks:"); + foreach (var f in feedbacks) + { + string val = ""; + string type = ""; + if (getCurrentStates) + { + if (f is BoolFeedback) + { + val = f.BoolValue.ToString(); + type = "boolean"; + } + else if (f is IntFeedback) + { + val = f.IntValue.ToString(); + type = "integer"; + } + else if (f is StringFeedback) + { + val = f.StringValue; + type = "string"; + } + } + Debug.Console(0, "{0,-12} {1, -25} {2}", type, + (string.IsNullOrEmpty(f.Cue.Name) ? "-no name-" : f.Cue.Name), val); + } + } + else + Debug.Console(0, source, "No available outputs:"); + } + } +======= +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public interface IHasFeedback : IKeyed + { + /// + /// This method shall return a list of all Output objects on a device, + /// including all "aggregate" devices. + /// + List Feedbacks { get; } + + } + + + public static class IHasFeedbackExtensions + { + public static void DumpFeedbacksToConsole(this IHasFeedback source, bool getCurrentStates) + { + var feedbacks = source.Feedbacks.OrderBy(x => x.Type); + if (feedbacks != null) + { + Debug.Console(0, source, "\n\nAvailable feedbacks:"); + foreach (var f in feedbacks) + { + string val = ""; + if (getCurrentStates) + { + if (f is BoolFeedback) + val = " = " + f.BoolValue; + else if(f is IntFeedback) + val = " = " + f.IntValue; + else if(f is StringFeedback) + val = " = " + f.StringValue; + + //switch (f.Type) + //{ + // case eCueType.Bool: + // val = " = " + f.BoolValue; + // break; + // case eCueType.Int: + // val = " = " + f.IntValue; + // break; + // case eCueType.String: + // val = " = " + f.StringValue; + // break; + // //case eOutputType.Other: + // // break; + //} + } + Debug.Console(0, "{0,-8} {1}{2}", f.GetType(), + (string.IsNullOrEmpty(f.Cue.Name) ? "-none-" : f.Cue.Name), val); + } + } + else + Debug.Console(0, source, "No available outputs:"); + } + } + + public static class IHasFeedbackFusionExtensions + { + + } +>>>>>>> origin/feature/ecs-307 +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_BASE_3692.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_BASE_3692.cs new file mode 100644 index 00000000..446577f6 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_BASE_3692.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public interface IHasFeedback : IKeyed + { + /// + /// This method shall return a list of all Output objects on a device, + /// including all "aggregate" devices. + /// + List Feedbacks { get; } + + } + + + public static class IHasFeedbackExtensions + { + public static void DumpFeedbacksToConsole(this IHasFeedback source, bool getCurrentStates) + { + var feedbacks = source.Feedbacks.OrderBy(x => x.Type); + if (feedbacks != null) + { + Debug.Console(0, source, "\n\nAvailable feedbacks:"); + foreach (var f in feedbacks) + { + string val = ""; + if (getCurrentStates) + { + if (f is BoolFeedback) + val = " = " + f.BoolValue; + else if(f is IntFeedback) + val = " = " + f.IntValue; + else if(f is StringFeedback) + val = " = " + f.StringValue; + + //switch (f.Type) + //{ + // case eCueType.Bool: + // val = " = " + f.BoolValue; + // break; + // case eCueType.Int: + // val = " = " + f.IntValue; + // break; + // case eCueType.String: + // val = " = " + f.StringValue; + // break; + // //case eOutputType.Other: + // // break; + //} + } + Debug.Console(0, "{0,-8} {1}{2}", f.GetType(), + (string.IsNullOrEmpty(f.Cue.Name) ? "-none-" : f.Cue.Name), val); + } + } + else + Debug.Console(0, source, "No available outputs:"); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_LOCAL_3692.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_LOCAL_3692.cs new file mode 100644 index 00000000..5d33afa9 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_LOCAL_3692.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharp.Reflection; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public interface IHasFeedback : IKeyed + { + /// + /// This method shall return a list of all Output objects on a device, + /// including all "aggregate" devices. + /// + List Feedbacks { get; } + + } + + + public static class IHasFeedbackExtensions + { + public static void DumpFeedbacksToConsole(this IHasFeedback source, bool getCurrentStates) + { + CType t = source.GetType(); + // get the properties and set them into a new collection of NameType wrappers + var props = t.GetProperties().Select(p => new PropertyNameType(p, t)); + + + + var feedbacks = source.Feedbacks.OrderBy(x => x.Type); + if (feedbacks != null) + { + Debug.Console(0, source, "\n\nAvailable feedbacks:"); + foreach (var f in feedbacks) + { + string val = ""; + string type = ""; + if (getCurrentStates) + { + if (f is BoolFeedback) + { + val = f.BoolValue.ToString(); + type = "boolean"; + } + else if (f is IntFeedback) + { + val = f.IntValue.ToString(); + type = "integer"; + } + else if (f is StringFeedback) + { + val = f.StringValue; + type = "string"; + } + } + Debug.Console(0, "{0,-12} {1, -25} {2}", type, + (string.IsNullOrEmpty(f.Cue.Name) ? "-no name-" : f.Cue.Name), val); + } + } + else + Debug.Console(0, source, "No available outputs:"); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_REMOTE_3692.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_REMOTE_3692.cs new file mode 100644 index 00000000..1ac02c71 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IHasFeedbacks_REMOTE_3692.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public interface IHasFeedback : IKeyed + { + /// + /// This method shall return a list of all Output objects on a device, + /// including all "aggregate" devices. + /// + List Feedbacks { get; } + + } + + + public static class IHasFeedbackExtensions + { + public static void DumpFeedbacksToConsole(this IHasFeedback source, bool getCurrentStates) + { + var feedbacks = source.Feedbacks.OrderBy(x => x.Type); + if (feedbacks != null) + { + Debug.Console(0, source, "\n\nAvailable feedbacks:"); + foreach (var f in feedbacks) + { + string val = ""; + if (getCurrentStates) + { + if (f is BoolFeedback) + val = " = " + f.BoolValue; + else if(f is IntFeedback) + val = " = " + f.IntValue; + else if(f is StringFeedback) + val = " = " + f.StringValue; + + //switch (f.Type) + //{ + // case eCueType.Bool: + // val = " = " + f.BoolValue; + // break; + // case eCueType.Int: + // val = " = " + f.IntValue; + // break; + // case eCueType.String: + // val = " = " + f.StringValue; + // break; + // //case eOutputType.Other: + // // break; + //} + } + Debug.Console(0, "{0,-8} {1}{2}", f.GetType(), + (string.IsNullOrEmpty(f.Cue.Name) ? "-none-" : f.Cue.Name), val); + } + } + else + Debug.Console(0, source, "No available outputs:"); + } + } + + public static class IHasFeedbackFusionExtensions + { + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IUsageTracking.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IUsageTracking.cs new file mode 100644 index 00000000..05b59366 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IUsageTracking.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public interface IUsageTracking + { + UsageTracking UsageTracker { get; set; } + } + + //public static class IUsageTrackingExtensions + //{ + // public static void EnableUsageTracker(this IUsageTracking device) + // { + // device.UsageTracker = new UsageTracking(); + // } + //} + + public class UsageTracking + { + public event EventHandler DeviceUsageEnded; + + public InUseTracking InUseTracker { get; protected set; } + + public bool UsageIsTracked { get; set; } + + public bool UsageTrackingStarted { get; protected set; } + public DateTime UsageStartTime { get; protected set; } + public DateTime UsageEndTime { get; protected set; } + + public Device Parent { get; private set; } + + public UsageTracking(Device parent) + { + Parent = parent; + + InUseTracker = new InUseTracking(); + + InUseTracker.InUseFeedback.OutputChange += InUseFeedback_OutputChange; //new EventHandler(); + } + + void InUseFeedback_OutputChange(object sender, EventArgs e) + { + if(InUseTracker.InUseFeedback.BoolValue) + { + StartDeviceUsage(); + } + else + { + EndDeviceUsage(); + } + } + + + /// + /// Stores the usage start time + /// + public void StartDeviceUsage() + { + UsageTrackingStarted = true; + UsageStartTime = DateTime.Now; + } + + /// + /// Calculates the difference between the usage start and end times, gets the total minutes used and fires an event to pass that info to a consumer + /// + public void EndDeviceUsage() + { + try + { + UsageTrackingStarted = false; + + UsageEndTime = DateTime.Now; + + if (UsageStartTime != null) + { + var timeUsed = UsageEndTime - UsageStartTime; + + var handler = DeviceUsageEnded; + + if (handler != null) + { + Debug.Console(1, "Device Usage Ended for: {0} at {1}. In use for {2} minutes.", Parent.Name, UsageEndTime, timeUsed.Minutes); + handler(this, new DeviceUsageEventArgs() { UsageEndTime = UsageEndTime, MinutesUsed = timeUsed.Minutes }); + } + } + } + catch (Exception e) + { + Debug.Console(1, "Error ending device usage: {0}", e); + } + } + } + + public class DeviceUsageEventArgs : EventArgs + { + public DateTime UsageEndTime { get; set; } + public int MinutesUsed { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IUsageTracking.cs.orig b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IUsageTracking.cs.orig new file mode 100644 index 00000000..59019df5 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IUsageTracking.cs.orig @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public interface IUsageTracking + { + UsageTracking UsageTracker { get; set; } + } + + //public static class IUsageTrackingExtensions + //{ + // public static void EnableUsageTracker(this IUsageTracking device) + // { + // device.UsageTracker = new UsageTracking(); + // } + //} + + public class UsageTracking + { + public event EventHandler DeviceUsageEnded; + + public InUseTracking InUseTracker { get; protected set; } + + public bool UsageIsTracked { get; set; } + public DateTime UsageStartTime { get; protected set; } + public DateTime UsageEndTime { get; protected set; } + + public Device Parent { get; private set; } + + public UsageTracking(Device parent) + { + Parent = parent; + + InUseTracker = new InUseTracking(); + + InUseTracker.InUseFeedback.OutputChange +=new EventHandler(InUseFeedback_OutputChange); + } + + void InUseFeedback_OutputChange(object sender, EventArgs e) + { + if(InUseTracker.InUseFeedback.BoolValue) + { + StartDeviceUsage(); + } + else + { + EndDeviceUsage(); + } + } + + + /// + /// Stores the usage start time + /// + public void StartDeviceUsage() + { + UsageStartTime = DateTime.Now; + } + + /// + /// Calculates the difference between the usage start and end times, gets the total minutes used and fires an event to pass that info to a consumer + /// + public void EndDeviceUsage() + { + try + { + UsageEndTime = DateTime.Now; + + if (UsageStartTime != null) + { + var timeUsed = UsageEndTime - UsageStartTime; + + var handler = DeviceUsageEnded; + + if (handler != null) + { + Debug.Console(1, "Device Usage Ended for: {0} at {1}. In use for {2} minutes.", Parent.Name, UsageEndTime, timeUsed.Minutes); + handler(this, new DeviceUsageEventArgs() { UsageEndTime = UsageEndTime, MinutesUsed = timeUsed.Minutes }); + } + } + } + catch (Exception e) + { +<<<<<<< HEAD + Debug.Console(1, "Device Usage Ended at {0}. In use for {1} minutes.", UsageEndTime, timeUsed.Minutes); + handler(this, new DeviceUsageEventArgs() { UsageEndTime = UsageEndTime, MinutesUsed = timeUsed.Minutes }); +======= + Debug.Console(1, "Error ending device usage: {0}", e); +>>>>>>> origin/feature/fusion-nyu + } + } + } + + public class DeviceUsageEventArgs : EventArgs + { + public DateTime UsageEndTime { get; set; } + public int MinutesUsed { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IVolumeAndAudioInterfaces.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IVolumeAndAudioInterfaces.cs new file mode 100644 index 00000000..1cff62fc --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IVolumeAndAudioInterfaces.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + /// + /// Defines minimal volume control methods + /// + public interface IBasicVolumeControls + { + void VolumeUp(bool pressRelease); + void VolumeDown(bool pressRelease); + void MuteToggle(); + } + + /// + /// Adds feedback and direct volume level set to IBasicVolumeControls + /// + public interface IBasicVolumeWithFeedback : IBasicVolumeControls + { + void SetVolume(ushort level); + void MuteOn(); + void MuteOff(); + IntFeedback VolumeLevelFeedback { get; } + BoolFeedback MuteFeedback { get; } + } + + /// + /// A class that implements this contains a reference to a current IBasicVolumeControls device. + /// The class may have multiple IBasicVolumeControls. + /// + public interface IHasCurrentVolumeControls + { + IBasicVolumeControls CurrentVolumeControls { get; } + event EventHandler CurrentVolumeDeviceChange; + } + + + /// + /// + /// + public interface IFullAudioSettings : IBasicVolumeWithFeedback + { + void SetBalance(ushort level); + void BalanceLeft(bool pressRelease); + void BalanceRight(bool pressRelease); + + void SetBass(ushort level); + void BassUp(bool pressRelease); + void BassDown(bool pressRelease); + + void SetTreble(ushort level); + void TrebleUp(bool pressRelease); + void TrebleDown(bool pressRelease); + + bool hasMaxVolume { get; } + void SetMaxVolume(ushort level); + void MaxVolumeUp(bool pressRelease); + void MaxVolumeDown(bool pressRelease); + + bool hasDefaultVolume { get; } + void SetDefaultVolume(ushort level); + void DefaultVolumeUp(bool pressRelease); + void DefaultVolumeDown(bool pressRelease); + + void LoudnessToggle(); + void MonoToggle(); + + BoolFeedback LoudnessFeedback { get; } + BoolFeedback MonoFeedback { get; } + IntFeedback BalanceFeedback { get; } + IntFeedback BassFeedback { get; } + IntFeedback TrebleFeedback { get; } + IntFeedback MaxVolumeFeedback { get; } + IntFeedback DefaultVolumeFeedback { get; } + } + + /// + /// A class that implements this, contains a reference to an IBasicVolumeControls device. + /// For example, speakers attached to an audio zone. The speakers can provide reference + /// to their linked volume control. + /// + public interface IHasVolumeDevice + { + IBasicVolumeControls VolumeDevice { get; } + } + + /// + /// Identifies a device that contains audio zones + /// + public interface IAudioZones : IRouting + { + Dictionary Zone { get; } + } + + /// + /// Defines minimum functionality for an audio zone + /// + public interface IAudioZone : IBasicVolumeWithFeedback + { + void SelectInput(ushort input); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IrOutputPortController.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IrOutputPortController.cs new file mode 100644 index 00000000..c67afe1c --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/IrOutputPortController.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + + /// + /// IR port wrapper. May act standalone + /// + public class IrOutputPortController : Device + { + uint IrPortUid; + IROutputPort IrPort; + + public ushort StandardIrPulseTime { get; set; } + public string DriverFilepath { get; private set; } + public bool DriverIsLoaded { get; private set; } + + /// + /// Constructor for IrDevice base class. If a null port is provided, this class will + /// still function without trying to talk to a port. + /// + public IrOutputPortController(string key, IROutputPort port, string irDriverFilepath) + : base(key) + { + //if (port == null) throw new ArgumentNullException("port"); + IrPort = port; + if (port == null) + { + Debug.Console(0, this, "WARNING No valid IR Port assigned to controller. IR will not function"); + return; + } + LoadDriver(irDriverFilepath); + } + + /// + /// Loads the IR driver at path + /// + /// + public void LoadDriver(string path) + { + if (string.IsNullOrEmpty(path)) path = DriverFilepath; + try + { + IrPortUid = IrPort.LoadIRDriver(path); + DriverFilepath = path; + StandardIrPulseTime = 200; + DriverIsLoaded = true; + } + catch + { + DriverIsLoaded = false; + var message = string.Format("WARNING IR Driver '{0}' failed to load", path); + Debug.Console(0, this, message); + ErrorLog.Error(message); + } + } + + + /// + /// Starts and stops IR command on driver. Safe for missing commands + /// + public virtual void PressRelease(string command, bool state) + { + Debug.Console(2, this, "IR:'{0}'={1}", command, state); + if (IrPort == null) + { + Debug.Console(2, this, "WARNING No IR Port assigned to controller"); + return; + } + if (!DriverIsLoaded) + { + Debug.Console(2, this, "WARNING IR driver is not loaded"); + return; + } + if (state) + { + if (IrPort.IsIRCommandAvailable(IrPortUid, command)) + IrPort.Press(IrPortUid, command); + else + NoIrCommandError(command); + } + else + IrPort.Release(); + } + + /// + /// Pulses a command on driver. Safe for missing commands + /// + public virtual void Pulse(string command, ushort time) + { + if (IrPort == null) + { + Debug.Console(2, this, "WARNING No IR Port assigned to controller"); + return; + } + if (!DriverIsLoaded) + { + Debug.Console(2, this, "WARNING IR driver is not loaded"); + return; + } + if (IrPort.IsIRCommandAvailable(IrPortUid, command)) + IrPort.PressAndRelease(IrPortUid, command, time); + else + NoIrCommandError(command); + } + + /// + /// Notifies the console when a bad command is used. + /// + protected void NoIrCommandError(string command) + { + Debug.Console(2, this, "Device {0}: IR Driver {1} does not contain command {2}", + Key, IrPort.IRDriverFileNameByIRDriverId(IrPortUid), command); + } + + + ///// + ///// When fed a dictionary of uint, string, will return UOs for each item, + ///// attached to this IrOutputPort + ///// + ///// + ///// + //public List GetUOsForIrCommands(Dictionary numStringDict) + //{ + // var funcs = new List(numStringDict.Count); + // foreach (var kvp in numStringDict) + // { + // // Have to assign these locally because of kvp's scope + // var cue = kvp.Key; + // var command = kvp.Value; + // funcs.Add(new BoolCueActionPair(cue, b => this.PressRelease(command, b))); + // } + // return funcs; + //} + + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/NewInterfaces.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/NewInterfaces.cs new file mode 100644 index 00000000..ca6524f4 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/NewInterfaces.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +namespace PepperDash.Essentials.Core +{ + //public interface IVolumeFunctions + //{ + // BoolCueActionPair VolumeUpCueActionPair { get; } + // BoolCueActionPair VolumeDownCueActionPair { get; } + // BoolCueActionPair MuteToggleCueActionPair { get; } + //} + + //public interface IVolumeTwoWay : IVolumeFunctions + //{ + // IntFeedback VolumeLevelFeedback { get; } + // UShortCueActionPair VolumeLevelCueActionPair { get; } + // BoolFeedback IsMutedFeedback { get; } + //} + + ///// + ///// + ///// + //public static class IFunctionListExtensions + //{ + // public static string GetFunctionsConsoleList(this IHasCueActionList device) + // { + // var sb = new StringBuilder(); + // var list = device.CueActionList; + // foreach (var cap in list) + // sb.AppendFormat("{0,-15} {1,4} {2}\r", cap.Cue.Name, cap.Cue.Number, cap.GetType().Name); + // return sb.ToString(); + // } + //} + + public enum AudioChangeType + { + Mute, Volume + } + + public class AudioChangeEventArgs + { + public AudioChangeType ChangeType { get; private set; } + public IBasicVolumeControls AudioDevice { get; private set; } + + public AudioChangeEventArgs(IBasicVolumeControls device, AudioChangeType changeType) + { + ChangeType = changeType; + AudioDevice = device; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/PresentationDeviceType.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/PresentationDeviceType.cs new file mode 100644 index 00000000..adb427fb --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/PresentationDeviceType.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.EthernetCommunication; +using Crestron.SimplSharpPro.UI; + +namespace PepperDash.Essentials.Core +{ + //[Obsolete] + //public class PresentationDeviceType + //{ + // public const ushort Default = 1; + // public const ushort CableSetTopBox = 2; + // public const ushort SatelliteSetTopBox = 3; + // public const ushort Dvd = 4; + // public const ushort Bluray = 5; + // public const ushort PC = 9; + // public const ushort Laptop = 10; + //} + + public enum PresentationSourceType + { + None, Dvd, Laptop, PC, SetTopBox, VCR + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/REMOVE IHasFeedbacks.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/REMOVE IHasFeedbacks.cs new file mode 100644 index 00000000..511a952b --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/REMOVE IHasFeedbacks.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + //public interface IHasFeedback : IKeyed + //{ + // /// + // /// This method shall return a list of all Output objects on a device, + // /// including all "aggregate" devices. + // /// + // List Feedbacks { get; } + + //} + + + //public static class IHasFeedbackExtensions + //{ + // public static void DumpFeedbacksToConsole(this IHasFeedback source, bool getCurrentStates) + // { + // var outputs = source.Feedbacks.OrderBy(x => x.Type); + // if (outputs != null) + // { + // Debug.Console(0, source, "\n\nAvailable outputs:"); + // foreach (var o in outputs) + // { + // string val = ""; + // if (getCurrentStates) + // { + // switch (o.Type) + // { + // case eCueType.Bool: + // val = " = " + o.BoolValue; + // break; + // case eCueType.Int: + // val = " = " + o.IntValue; + // break; + // case eCueType.String: + // val = " = " + o.StringValue; + // break; + // //case eOutputType.Other: + // // break; + // } + // } + // Debug.Console(0, "{0,-8} {1,5} {2}{3}", o.Type, o.Cue.Number, + // (string.IsNullOrEmpty(o.Cue.Name) ? "-none-" : o.Cue.Name), val); + // } + // } + // else + // Debug.Console(0, source, "No available outputs:"); + // } + //} +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/ReconfigurableDevice.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/ReconfigurableDevice.cs new file mode 100644 index 00000000..3acfcac0 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/ReconfigurableDevice.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.Core.Devices +{ + /// + /// + /// + public abstract class ReconfigurableDevice : Device + { + public event EventHandler ConfigChanged; + + public DeviceConfig Config { get; private set; } + + public ReconfigurableDevice(DeviceConfig config) + : base(config.Key) + { + SetNameHelper(config); + + Config = config; + } + + /// + /// Sets the Config, calls CustomSetConfig and fires the ConfigChanged event + /// + /// + public void SetConfig(DeviceConfig config) + { + Config = config; + + SetNameHelper(config); + + CustomSetConfig(config); + + var handler = ConfigChanged; + if (handler != null) + { + handler(this, new EventArgs()); + } + } + + void SetNameHelper(DeviceConfig config) + { + if (!string.IsNullOrEmpty(config.Name)) + Name = config.Name; + } + + /// + /// Used by the extending class to allow for any custom actions to be taken (tell the ConfigWriter to write config, etc) + /// + /// + protected virtual void CustomSetConfig(DeviceConfig config) + { + ConfigWriter.UpdateDeviceConfig(config); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/SmartObjectBaseTypes.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/SmartObjectBaseTypes.cs new file mode 100644 index 00000000..37e62036 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/SmartObjectBaseTypes.cs @@ -0,0 +1,11 @@ + +namespace PepperDash.Essentials.Core +{ + public class SmartObjectJoinOffsets + { + public const ushort Dpad = 1; + public const ushort Numpad = 2; + + public const ushort PresetList = 6; + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/SourceListItem.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/SourceListItem.cs new file mode 100644 index 00000000..86c940d3 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/SourceListItem.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharpPro; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public enum eSourceListItemType + { + Route, Off, Other, SomethingAwesomerThanThese + } + + /// + /// Represents an item in a source list - can be deserialized into. + /// + public class SourceListItem + { + [JsonProperty("sourceKey")] + public string SourceKey { get; set; } + + /// + /// Returns the source Device for this, if it exists in DeviceManager + /// + [JsonIgnore] + public Device SourceDevice + { + get + { + if (_SourceDevice == null) + _SourceDevice = DeviceManager.GetDeviceForKey(SourceKey) as Device; + return _SourceDevice; + } + } + Device _SourceDevice; + + /// + /// Gets either the source's Name or this AlternateName property, if + /// defined. If source doesn't exist, returns "Missing source" + /// + [JsonProperty("preferredName")] + public string PreferredName + { + get + { + if (string.IsNullOrEmpty(Name)) + { + if (SourceDevice == null) + return "---"; + return SourceDevice.Name; + } + return Name; + } + } + + /// + /// A name that will override the source's name on the UI + /// + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("icon")] + public string Icon { get; set; } + + [JsonProperty("altIcon")] + public string AltIcon { get; set; } + + [JsonProperty("includeInSourceList")] + public bool IncludeInSourceList { get; set; } + + [JsonProperty("order")] + public int Order { get; set; } + + [JsonProperty("volumeControlKey")] + public string VolumeControlKey { get; set; } + + [JsonProperty("type")] + [JsonConverter(typeof(StringEnumConverter))] + public eSourceListItemType Type { get; set; } + + [JsonProperty("routeList")] + public List RouteList { get; set; } + + [JsonProperty("disableCodecSharing")] + public bool DisableCodecSharing { get; set; } + + [JsonProperty("disableRoutedSharing")] + public bool DisableRoutedSharing { get; set; } + + public SourceListItem() + { + Icon = "Blank"; + } + } + + public class SourceRouteListItem + { + [JsonProperty("sourceKey")] + public string SourceKey { get; set; } + + [JsonProperty("destinationKey")] + public string DestinationKey { get; set; } + + [JsonProperty("type")] + public eRoutingSignalType Type { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/VolumeDeviceChangeEventArgs.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/VolumeDeviceChangeEventArgs.cs new file mode 100644 index 00000000..2171cb73 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Devices/VolumeDeviceChangeEventArgs.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public class VolumeDeviceChangeEventArgs : EventArgs + { + public IBasicVolumeControls OldDev { get; private set; } + public IBasicVolumeControls NewDev { get; private set; } + public ChangeType Type { get; private set; } + + public VolumeDeviceChangeEventArgs(IBasicVolumeControls oldDev, IBasicVolumeControls newDev, ChangeType type) + { + OldDev = oldDev; + NewDev = newDev; + Type = type; + } + } + + /// + /// + /// + public enum ChangeType + { + WillChange, DidChange + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/BasicIrDisplay.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/BasicIrDisplay.cs new file mode 100644 index 00000000..b22f04d0 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/BasicIrDisplay.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Core +{ + public class BasicIrDisplay : DisplayBase, IBasicVolumeControls, IPower, IWarmingCooling, IRoutingSinkWithSwitching + { + public IrOutputPortController IrPort { get; private set; } + public ushort IrPulseTime { get; set; } + + protected override Func PowerIsOnFeedbackFunc + { + get { return () => _PowerIsOn; } + } + protected override Func IsCoolingDownFeedbackFunc + { + get { return () => _IsCoolingDown; } + } + protected override Func IsWarmingUpFeedbackFunc + { + get { return () => _IsWarmingUp; } + } + + bool _PowerIsOn; + bool _IsWarmingUp; + bool _IsCoolingDown; + + public BasicIrDisplay(string key, string name, IROutputPort port, string irDriverFilepath) + : base(key, name) + { + IrPort = new IrOutputPortController(key + "-ir", port, irDriverFilepath); + DeviceManager.AddDevice(IrPort); + + PowerIsOnFeedback.OutputChange += (o, a) => { + Debug.Console(2, this, "Power on={0}", _PowerIsOn); + if (_PowerIsOn) StartWarmingTimer(); + else StartCoolingTimer(); + }; + IsWarmingUpFeedback.OutputChange += (o, a) => Debug.Console(2, this, "Warming up={0}", _IsWarmingUp); + IsCoolingDownFeedback.OutputChange += (o, a) => Debug.Console(2, this, "Cooling down={0}", _IsCoolingDown); + + InputPorts.AddRange(new RoutingPortCollection + { + new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(Hdmi1), this, false), + new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(Hdmi2), this, false), + new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(Hdmi3), this, false), + new RoutingInputPort(RoutingPortNames.HdmiIn4, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(Hdmi4), this, false), + new RoutingInputPort(RoutingPortNames.ComponentIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(Component1), this, false), + new RoutingInputPort(RoutingPortNames.CompositeIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(Video1), this, false), + new RoutingInputPort(RoutingPortNames.AntennaIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(Antenna), this, false), + }); + } + + public void Hdmi1() + { + IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_1, IrPulseTime); + } + + public void Hdmi2() + { + IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_2, IrPulseTime); + } + + public void Hdmi3() + { + IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_3, IrPulseTime); + } + + public void Hdmi4() + { + IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_4, IrPulseTime); + } + + public void Component1() + { + IrPort.Pulse(IROutputStandardCommands.IROut_COMPONENT_1, IrPulseTime); + } + + public void Video1() + { + IrPort.Pulse(IROutputStandardCommands.IROut_VIDEO_1, IrPulseTime); + } + + public void Antenna() + { + IrPort.Pulse(IROutputStandardCommands.IROut_ANTENNA, IrPulseTime); + } + + #region IPower Members + + public override void PowerOn() + { + IrPort.Pulse(IROutputStandardCommands.IROut_POWER_ON, IrPulseTime); + _PowerIsOn = true; + PowerIsOnFeedback.FireUpdate(); + } + + public override void PowerOff() + { + _PowerIsOn = false; + PowerIsOnFeedback.FireUpdate(); + IrPort.Pulse(IROutputStandardCommands.IROut_POWER_OFF, IrPulseTime); + } + + public override void PowerToggle() + { + _PowerIsOn = false; + PowerIsOnFeedback.FireUpdate(); + IrPort.Pulse(IROutputStandardCommands.IROut_POWER, IrPulseTime); + } + + #endregion + + #region IBasicVolumeControls Members + + public void VolumeUp(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_VOL_PLUS, pressRelease); + } + + public void VolumeDown(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_VOL_MINUS, pressRelease); + } + + public void MuteToggle() + { + IrPort.Pulse(IROutputStandardCommands.IROut_MUTE, 200); + } + + #endregion + + void StartWarmingTimer() + { + _IsWarmingUp = true; + IsWarmingUpFeedback.FireUpdate(); + new CTimer(o => { + _IsWarmingUp = false; + IsWarmingUpFeedback.FireUpdate(); + }, 10000); + } + + void StartCoolingTimer() + { + _IsCoolingDown = true; + IsCoolingDownFeedback.FireUpdate(); + new CTimer(o => + { + _IsCoolingDown = false; + IsCoolingDownFeedback.FireUpdate(); + }, 7000); + } + + #region IRoutingSink Members + + /// + /// Typically called by the discovery routing algorithm. + /// + /// A delegate containing the input selector method to call + public override void ExecuteSwitch(object inputSelector) + { + Debug.Console(2, this, "Switching to input '{0}'", (inputSelector as Action).ToString()); + + Action finishSwitch = () => + { + var action = inputSelector as Action; + if (action != null) + action(); + }; + + if (!PowerIsOnFeedback.BoolValue) + { + PowerOn(); + EventHandler oneTimer = null; + oneTimer = (o, a) => + { + if (IsWarmingUpFeedback.BoolValue) return; // Only catch done warming + IsWarmingUpFeedback.OutputChange -= oneTimer; + finishSwitch(); + }; + IsWarmingUpFeedback.OutputChange += oneTimer; + } + else // Do it! + finishSwitch(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/DELETE IRDisplayBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/DELETE IRDisplayBase.cs new file mode 100644 index 00000000..06b4da07 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/DELETE IRDisplayBase.cs @@ -0,0 +1,105 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; + +//namespace PepperDash.Essentials.Core +//{ +// public abstract class IRDisplayBase : DisplayBase, IHasCueActionList +// { +// public IrOutputPortController IrPort { get; private set; } +// /// +// /// Default to 200ms +// /// +// public ushort IrPulseTime { get; set; } +// bool _PowerIsOn; +// bool _IsWarmingUp; +// bool _IsCoolingDown; + +// /// +// /// FunctionList is pre-defined to have power commands. +// /// +// public IRDisplayBase(string key, string name, IROutputPort port, string irDriverFilepath) +// : base(key, name) +// { +// IrPort = new IrOutputPortController("ir-" + key, port, irDriverFilepath); +// IrPulseTime = 200; +// WarmupTime = 7000; +// CooldownTime = 10000; + +// CueActionList = new List +// { +// new BoolCueActionPair(CommonBoolCue.Power, b=> PowerToggle()), +// new BoolCueActionPair(CommonBoolCue.PowerOn, b=> PowerOn()), +// new BoolCueActionPair(CommonBoolCue.PowerOff, b=> PowerOff()), +// }; +// } + +// public override void PowerOn() +// { +// if (!PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) +// { +// _IsWarmingUp = true; +// IsWarmingUpFeedback.FireUpdate(); +// // Fake power-up cycle +// WarmupTimer = new CTimer(o => +// { +// _IsWarmingUp = false; +// _PowerIsOn = true; +// IsWarmingUpFeedback.FireUpdate(); +// PowerIsOnFeedback.FireUpdate(); +// }, WarmupTime); +// } +// IrPort.Pulse(IROutputStandardCommands.IROut_POWER_ON, IrPulseTime); +// } + +// public override void PowerOff() +// { +// // If a display has unreliable-power off feedback, just override this and +// // remove this check. +// if (PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) +// { +// _IsCoolingDown = true; +// _PowerIsOn = false; +// PowerIsOnFeedback.FireUpdate(); +// IsCoolingDownFeedback.FireUpdate(); +// // Fake cool-down cycle +// CooldownTimer = new CTimer(o => +// { +// _IsCoolingDown = false; +// IsCoolingDownFeedback.FireUpdate(); +// }, CooldownTime); +// } +// IrPort.Pulse(IROutputStandardCommands.IROut_POWER_OFF, IrPulseTime); +// } + +// public override void PowerToggle() +// { +// // Not sure how to handle the feedback, but we should default to power off fb. +// // Does this need to trigger feedback?? +// _PowerIsOn = false; +// IrPort.Pulse(IROutputStandardCommands.IROut_POWER, IrPulseTime); +// } + +// #region IFunctionList Members + +// public List CueActionList +// { +// get; +// private set; +// } + +// #endregion + +// protected override Func PowerIsOnOutputFunc { get { return () => _PowerIsOn; } } +// protected override Func IsCoolingDownOutputFunc { get { return () => _IsCoolingDown; } } +// protected override Func IsWarmingUpOutputFunc { get { return () => _IsWarmingUp; } } + +// public override void ExecuteSwitch(object selector) +// { +// IrPort.Pulse((string)selector, IrPulseTime); +// } +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/DisplayBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/DisplayBase.cs new file mode 100644 index 00000000..9ef0ee86 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/DisplayBase.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public abstract class DisplayBase : Device, IHasFeedback, IRoutingSinkWithSwitching, IPower, IWarmingCooling, IUsageTracking + { + public BoolFeedback PowerIsOnFeedback { get; protected set; } + public BoolFeedback IsCoolingDownFeedback { get; protected set; } + public BoolFeedback IsWarmingUpFeedback { get; private set; } + + public UsageTracking UsageTracker { get; set; } + + public uint WarmupTime { get; set; } + public uint CooldownTime { get; set; } + + /// + /// Bool Func that will provide a value for the PowerIsOn Output. Must be implemented + /// by concrete sub-classes + /// + abstract protected Func PowerIsOnFeedbackFunc { get; } + abstract protected Func IsCoolingDownFeedbackFunc { get; } + abstract protected Func IsWarmingUpFeedbackFunc { get; } + + + protected CTimer WarmupTimer; + protected CTimer CooldownTimer; + + #region IRoutingInputs Members + + public RoutingPortCollection InputPorts { get; private set; } + + #endregion + + public DisplayBase(string key, string name) + : base(key, name) + { + PowerIsOnFeedback = new BoolFeedback("PowerOnFeedback", PowerIsOnFeedbackFunc); + IsCoolingDownFeedback = new BoolFeedback("IsCoolingDown", IsCoolingDownFeedbackFunc); + IsWarmingUpFeedback = new BoolFeedback("IsWarmingUp", IsWarmingUpFeedbackFunc); + + InputPorts = new RoutingPortCollection(); + + PowerIsOnFeedback.OutputChange += PowerIsOnFeedback_OutputChange; + } + + void PowerIsOnFeedback_OutputChange(object sender, EventArgs e) + { + if (UsageTracker != null) + { + if (PowerIsOnFeedback.BoolValue) + UsageTracker.StartDeviceUsage(); + else + UsageTracker.EndDeviceUsage(); + } + } + + public abstract void PowerOn(); + public abstract void PowerOff(); + public abstract void PowerToggle(); + + public virtual FeedbackCollection Feedbacks + { + get + { + return new FeedbackCollection + { + PowerIsOnFeedback, + IsCoolingDownFeedback, + IsWarmingUpFeedback + }; + } + } + + public abstract void ExecuteSwitch(object selector); + + } + + /// + /// + /// + public abstract class TwoWayDisplayBase : DisplayBase + { + public StringFeedback CurrentInputFeedback { get; private set; } + + abstract protected Func CurrentInputFeedbackFunc { get; } + + + public static MockDisplay DefaultDisplay + { + get + { + if (_DefaultDisplay == null) + _DefaultDisplay = new MockDisplay("default", "Default Display"); + return _DefaultDisplay; + } + } + static MockDisplay _DefaultDisplay; + + public TwoWayDisplayBase(string key, string name) + : base(key, name) + { + CurrentInputFeedback = new StringFeedback(CurrentInputFeedbackFunc); + + WarmupTime = 7000; + CooldownTime = 15000; + + Feedbacks.Add(CurrentInputFeedback); + + + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/MockDisplay.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/MockDisplay.cs new file mode 100644 index 00000000..82735d0f --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Display/MockDisplay.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using PepperDash.Core; +using PepperDash.Essentials.Core.Routing; + + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback + + { + public RoutingInputPort HdmiIn1 { get; private set; } + public RoutingInputPort HdmiIn2 { get; private set; } + public RoutingInputPort HdmiIn3 { get; private set; } + public RoutingInputPort ComponentIn1 { get; private set; } + public RoutingInputPort VgaIn1 { get; private set; } + + bool _PowerIsOn; + bool _IsWarmingUp; + bool _IsCoolingDown; + + protected override Func PowerIsOnFeedbackFunc { get { return () => _PowerIsOn; } } + protected override Func IsCoolingDownFeedbackFunc { get { return () => _IsCoolingDown; } } + protected override Func IsWarmingUpFeedbackFunc { get { return () => _IsWarmingUp; } } + protected override Func CurrentInputFeedbackFunc { get { return () => "Not Implemented"; } } + + int VolumeHeldRepeatInterval = 200; + ushort VolumeInterval = 655; + ushort _FakeVolumeLevel = 31768; + bool _IsMuted; + + public MockDisplay(string key, string name) + : base(key, name) + { + HdmiIn1 = new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + HdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + HdmiIn3 = new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + ComponentIn1 = new RoutingInputPort(RoutingPortNames.ComponentIn, eRoutingSignalType.Video, + eRoutingPortConnectionType.Component, null, this); + VgaIn1 = new RoutingInputPort(RoutingPortNames.VgaIn, eRoutingSignalType.Video, + eRoutingPortConnectionType.Composite, null, this); + InputPorts.AddRange(new[] { HdmiIn1, HdmiIn2, HdmiIn3, ComponentIn1, VgaIn1 }); + + VolumeLevelFeedback = new IntFeedback(() => { return _FakeVolumeLevel; }); + MuteFeedback = new BoolFeedback("MuteOn", () => _IsMuted); + } + + public override void PowerOn() + { + if (!PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) + { + _IsWarmingUp = true; + IsWarmingUpFeedback.InvokeFireUpdate(); + // Fake power-up cycle + WarmupTimer = new CTimer(o => + { + _IsWarmingUp = false; + _PowerIsOn = true; + IsWarmingUpFeedback.InvokeFireUpdate(); + PowerIsOnFeedback.InvokeFireUpdate(); + }, WarmupTime); + } + } + + public override void PowerOff() + { + // If a display has unreliable-power off feedback, just override this and + // remove this check. + if (PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) + { + _IsCoolingDown = true; + _PowerIsOn = false; + PowerIsOnFeedback.InvokeFireUpdate(); + IsCoolingDownFeedback.InvokeFireUpdate(); + // Fake cool-down cycle + CooldownTimer = new CTimer(o => + { + Debug.Console(2, this, "Cooldown timer ending"); + _IsCoolingDown = false; + IsCoolingDownFeedback.InvokeFireUpdate(); + }, CooldownTime); + } + } + + public override void PowerToggle() + { + if (PowerIsOnFeedback.BoolValue && !IsWarmingUpFeedback.BoolValue) + PowerOff(); + else if (!PowerIsOnFeedback.BoolValue && !IsCoolingDownFeedback.BoolValue) + PowerOn(); + } + + public override void ExecuteSwitch(object selector) + { + Debug.Console(2, this, "ExecuteSwitch: {0}", selector); + } + + + + #region IBasicVolumeWithFeedback Members + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public void SetVolume(ushort level) + { + _FakeVolumeLevel = level; + VolumeLevelFeedback.InvokeFireUpdate(); + } + + public void MuteOn() + { + _IsMuted = true; + MuteFeedback.InvokeFireUpdate(); + } + + public void MuteOff() + { + _IsMuted = false; + MuteFeedback.InvokeFireUpdate(); + } + + public BoolFeedback MuteFeedback { get; private set; } + + #endregion + + #region IBasicVolumeControls Members + + public void VolumeUp(bool pressRelease) + { + //while (pressRelease) + //{ + Debug.Console(2, this, "Volume Down {0}", pressRelease); + if (pressRelease) + { + var newLevel = _FakeVolumeLevel + VolumeInterval; + SetVolume((ushort)newLevel); + CrestronEnvironment.Sleep(VolumeHeldRepeatInterval); + } + //} + } + + public void VolumeDown(bool pressRelease) + { + //while (pressRelease) + //{ + Debug.Console(2, this, "Volume Up {0}", pressRelease); + if (pressRelease) + { + var newLevel = _FakeVolumeLevel - VolumeInterval; + SetVolume((ushort)newLevel); + CrestronEnvironment.Sleep(VolumeHeldRepeatInterval); + } + //} + } + + public void MuteToggle() + { + _IsMuted = !_IsMuted; + MuteFeedback.InvokeFireUpdate(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ethernet/EthernetStatistics.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ethernet/EthernetStatistics.cs new file mode 100644 index 00000000..13e49b5c --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ethernet/EthernetStatistics.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Core.Ethernet +{ + public static class EthernetSettings + { + public static readonly BoolFeedback LinkActive = new BoolFeedback("LinkActive", + () => true); + public static readonly BoolFeedback DhcpActive = new BoolFeedback("DhcpActive", + () => CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, 0) == "ON"); + + + public static readonly StringFeedback Hostname = new StringFeedback("Hostname", + () => CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0)); + public static readonly StringFeedback IpAddress0 = new StringFeedback("IpAddress0", + () => CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)); + public static readonly StringFeedback SubnetMask0 = new StringFeedback("SubnetMask0", + () => CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, 0)); + public static readonly StringFeedback DefaultGateway0 = new StringFeedback("DefaultGateway0", + () => CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, 0)); + } + + public static class EthernetCue + { + public static readonly Cue LinkActive = Cue.BoolCue("LinkActive", 1); + public static readonly Cue DhcpActive = Cue.BoolCue("DhcpActive", 2); + + public static readonly Cue Hostname = Cue.StringCue("Hostname", 1); + public static readonly Cue IpAddress0 = Cue.StringCue("IpAddress0", 2); + public static readonly Cue SubnetMask0 = Cue.StringCue("SubnetMask0", 3); + public static readonly Cue DefaultGateway0 = Cue.StringCue("DefaultGateway0", 4); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Factory/DeviceFactory.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Factory/DeviceFactory.cs new file mode 100644 index 00000000..cfe5af0a --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Factory/DeviceFactory.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.Core +{ + public class DeviceFactory + { + /// + /// A dictionary of factory methods, keyed by config types, added by plugins. + /// These methods are looked up and called by GetDevice in this class. + /// + static Dictionary> FactoryMethods = + new Dictionary>(StringComparer.OrdinalIgnoreCase); + + /// + /// Adds a plugin factory method + /// + /// + /// + public static void AddFactoryForType(string type, Func method) + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Adding factory method for type '{0}'", type); + DeviceFactory.FactoryMethods.Add(type, method); + } + + /// + /// The factory method for Core "things". Also iterates the Factory methods that have + /// been loaded from plugins + /// + /// + /// + public static IKeyed GetDevice(DeviceConfig dc) + { + var key = dc.Key; + var name = dc.Name; + var type = dc.Type; + var properties = dc.Properties; + + var typeName = dc.Type.ToLower(); + + // Check "core" types first + if (typeName == "genericcomm") + { + Debug.Console(1, "Factory Attempting to create new Generic Comm Device"); + return new GenericComm(dc); + } + + // then check for types that have been added by plugin dlls. + if (FactoryMethods.ContainsKey(typeName)) + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Loading '{0}' from plugin", dc.Type); + return FactoryMethods[typeName](dc); + } + + return null; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedback.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedback.cs new file mode 100644 index 00000000..a5be1fb4 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedback.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +namespace PepperDash.Essentials.Core +{ + /// + /// A Feedback whose output is derived from the return value of a provided Func. + /// + public class BoolFeedback : Feedback + { + /// + /// Returns the current value of the feedback, derived from the ValueFunc. The ValueFunc is + /// evaluated whenever FireUpdate() is called + /// + public override bool BoolValue { get { return _BoolValue; } } + bool _BoolValue; + + public override eCueType Type { get { return eCueType.Bool; } } + + /// + /// Fake value to be used in test mode + /// + public bool TestValue { get; private set; } + + /// + /// Func that evaluates on FireUpdate + /// + public Func ValueFunc { get; private set; } + + List LinkedInputSigs = new List(); + List LinkedComplementInputSigs = new List(); + + public BoolFeedback(Func valueFunc) + : this(null, valueFunc) + { + } + + public BoolFeedback(string key, Func valueFunc) + : base(key) + { + ValueFunc = valueFunc; + } + + //public BoolFeedback(Cue cue, Func valueFunc) + // : base(cue) + //{ + // if (cue == null) throw new ArgumentNullException("cue"); + // ValueFunc = valueFunc; + //} + + public override void FireUpdate() + { + bool newValue = InTestMode ? TestValue : ValueFunc.Invoke(); + if (newValue != _BoolValue) + { + _BoolValue = newValue; + LinkedInputSigs.ForEach(s => UpdateSig(s)); + LinkedComplementInputSigs.ForEach(s => UpdateComplementSig(s)); + OnOutputChange(newValue); + } + } + + public void LinkInputSig(BoolInputSig sig) + { + LinkedInputSigs.Add(sig); + UpdateSig(sig); + } + + public void UnlinkInputSig(BoolInputSig sig) + { + LinkedInputSigs.Remove(sig); + } + + public void LinkComplementInputSig(BoolInputSig sig) + { + LinkedComplementInputSigs.Add(sig); + UpdateComplementSig(sig); + } + + public void UnlinkComplementInputSig(BoolInputSig sig) + { + LinkedComplementInputSigs.Remove(sig); + } + + public override string ToString() + { + return (InTestMode ? "TEST -- " : "") + BoolValue.ToString(); + } + + /// + /// Puts this in test mode, sets the test value and fires an update. + /// + /// + public void SetTestValue(bool value) + { + TestValue = value; + InTestMode = true; + FireUpdate(); + } + + void UpdateSig(BoolInputSig sig) + { + sig.BoolValue = _BoolValue; + } + + void UpdateComplementSig(BoolInputSig sig) + { + sig.BoolValue = !_BoolValue; + } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedbackOneShot.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedbackOneShot.cs new file mode 100644 index 00000000..cc183569 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedbackOneShot.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +namespace PepperDash.Essentials.Core +{ + public class BoolFeedbackPulse + { + public uint TimeoutMs { get; set; } + + /// + /// Defaults to false + /// + public bool CanRetrigger { get; set; } + + public BoolFeedback Feedback { get; private set; } + CTimer Timer; + + bool _BoolValue; + + /// + /// Creates a non-retriggering one shot + /// + public BoolFeedbackPulse(uint timeoutMs) + : this(timeoutMs, false) + { + } + + /// + /// Create a retriggerable one shot by setting canRetrigger true + /// + public BoolFeedbackPulse(uint timeoutMs, bool canRetrigger) + { + TimeoutMs = timeoutMs; + CanRetrigger = canRetrigger; + Feedback = new BoolFeedback(() => _BoolValue); + } + + /// + /// Starts the + /// + /// + public void Start() + { + if (Timer == null) + { + _BoolValue = true; + Feedback.FireUpdate(); + Timer = new CTimer(o => + { + _BoolValue = false; + Feedback.FireUpdate(); + Timer = null; + }, TimeoutMs); + } + // Timer is running, if retrigger is set, reset it. + else if (CanRetrigger) + Timer.Reset(TimeoutMs); + } + + public void Cancel() + { + if(Timer != null) + Timer.Reset(0); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedbackPulseExtender.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedbackPulseExtender.cs new file mode 100644 index 00000000..53a5e559 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolFeedbackPulseExtender.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +namespace PepperDash.Essentials.Core +{ + /// + /// A class that wraps a BoolFeedback with logic that extends it's true state for + /// a time period after the value goes false. + /// + public class BoolFeedbackPulseExtender + { + public uint TimeoutMs { get; set; } + public BoolFeedback Feedback { get; private set; } + CTimer Timer; + + /// + /// When set to true, will cause Feedback to go high, and cancel the timer. + /// When false, will start the timer, and after timeout, will go low and + /// feedback will go low. + /// + public bool BoolValue + { + get { return _BoolValue; } + set + { + if (value) + { // if Timer is running and the value goes high, cancel it. + if (Timer != null) + { + Timer.Stop(); + Timer = null; + } + // if it's already true, don't fire again + if (_BoolValue == true) + return; + _BoolValue = true; + Feedback.FireUpdate(); + } + else + { + if (Timer == null) + Timer = new CTimer(o => ClearFeedback(), TimeoutMs); + } + } + } + bool _BoolValue; + + /// + /// Constructor + /// + /// The time which the true state will be extended after set to false + public BoolFeedbackPulseExtender(uint timeoutMs) + { + TimeoutMs = timeoutMs; + Feedback = new BoolFeedback(() => this.BoolValue); + } + + /// + /// Forces the feedback to false regardless of timeout + /// + public void ClearNow() + { + if (Timer != null) + Timer.Stop(); + ClearFeedback(); + } + + void ClearFeedback() + { + _BoolValue = false; + Feedback.FireUpdate(); + Timer = null; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolOutputLogicals.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolOutputLogicals.cs new file mode 100644 index 00000000..3074254e --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/BoolOutputLogicals.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +namespace PepperDash.Essentials.Core +{ + + + public abstract class BoolFeedbackLogic + { + /// + /// Output representing the "and" value of all connected inputs + /// + public BoolFeedback Output { get; private set; } + + /// + /// List of all connected outputs + /// + protected List OutputsIn = new List(); + + protected bool ComputedValue; + + public BoolFeedbackLogic() + { + Output = new BoolFeedback(() => ComputedValue); + } + + public void AddOutputIn(BoolFeedback output) + { + // Don't double up outputs + if(OutputsIn.Contains(output)) return; + + OutputsIn.Add(output); + output.OutputChange += AnyInput_OutputChange; + Evaluate(); + } + + public void AddOutputsIn(List outputs) + { + foreach (var o in outputs) + { + // skip existing + if (OutputsIn.Contains(o)) continue; + + OutputsIn.Add(o); + o.OutputChange += AnyInput_OutputChange; + } + Evaluate(); + } + + public void RemoveOutputIn(BoolFeedback output) + { + // Don't double up outputs + if (OutputsIn.Contains(output)) return; + + OutputsIn.Remove(output); + output.OutputChange -= AnyInput_OutputChange; + Evaluate(); + } + + public void RemoveOutputsIn(List outputs) + { + foreach (var o in outputs) + { + OutputsIn.Remove(o); + o.OutputChange -= AnyInput_OutputChange; + } + Evaluate(); + } + + void AnyInput_OutputChange(object sender, EventArgs e) + { + Evaluate(); + } + + protected abstract void Evaluate(); + } + + public class BoolFeedbackAnd : BoolFeedbackLogic + { + protected override void Evaluate() + { + var prevValue = ComputedValue; + var newValue = OutputsIn.All(o => o.BoolValue); + if (newValue != prevValue) + { + ComputedValue = newValue; + Output.FireUpdate(); + } + } + } + + public class BoolFeedbackOr : BoolFeedbackLogic + { + protected override void Evaluate() + { + var prevValue = ComputedValue; + var newValue = OutputsIn.Any(o => o.BoolValue); + if (newValue != prevValue) + { + ComputedValue = newValue; + Output.FireUpdate(); + } + } + } + + public class BoolFeedbackLinq : BoolFeedbackLogic + { + Func, bool> Predicate; + + public BoolFeedbackLinq(Func, bool> predicate) + : base() + { + Predicate = predicate; + } + + protected override void Evaluate() + { + var prevValue = ComputedValue; + var newValue = Predicate(OutputsIn); + if (newValue != prevValue) + { + ComputedValue = newValue; + Output.FireUpdate(); + } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackBase.cs new file mode 100644 index 00000000..96571dce --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackBase.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public abstract class Feedback : IKeyed + { + public event EventHandler OutputChange; + + public string Key { get; private set; } + + public virtual bool BoolValue { get { return false; } } + public virtual int IntValue { get { return 0; } } + public virtual string StringValue { get { return ""; } } + public virtual string SerialValue { get { return ""; } } + + //public Cue Cue { get; private set; } + + public abstract eCueType Type { get; } + + /// + /// Feedbacks can be put into test mode for simulation of events without real data. + /// Using JSON debugging methods and the Set/ClearTestValue methods, we can simulate + /// Feedback behaviors + /// + public bool InTestMode { get; protected set; } + + /// + /// Base Constructor - empty + /// + protected Feedback() + { + } + + protected Feedback(string key) + { + if (key == null) + Key = ""; + else + Key = key; + } + + //protected Feedback(Cue cue) + //{ + // Cue = cue; + //} + + /// + /// Clears test mode and fires update. + /// + public void ClearTestValue() + { + InTestMode = false; + FireUpdate(); + } + + /// + /// Fires an update synchronously + /// + public abstract void FireUpdate(); + + /// + /// Fires the update asynchronously within a CrestronInvoke + /// + public void InvokeFireUpdate() + { + CrestronInvoke.BeginInvoke(o => FireUpdate()); + } + + ///// + ///// Helper method that fires event. Use this intstead of calling OutputChange + ///// + //protected void OnOutputChange() + //{ + // if (OutputChange != null) OutputChange(this, EventArgs.Empty); + //} + + protected void OnOutputChange(bool value) + { + if (OutputChange != null) OutputChange(this, new FeedbackEventArgs(value)); + } + + protected void OnOutputChange(int value) + { + if (OutputChange != null) OutputChange(this, new FeedbackEventArgs(value)); + } + + + protected void OnOutputChange(string value) + { + if (OutputChange != null) OutputChange(this, new FeedbackEventArgs(value)); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackCollection.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackCollection.cs new file mode 100644 index 00000000..ed7517da --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackCollection.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + /// + /// Basically a List , with an indexer to find feedbacks by key name + /// + public class FeedbackCollection : List where T : Feedback + { + /// + /// Case-insensitive port lookup linked to feedbacks' keys + /// + public T this[string key] + { + get + { + return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackEventArgs.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackEventArgs.cs new file mode 100644 index 00000000..9a7f5c29 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/FeedbackEventArgs.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + public class FeedbackEventArgs : EventArgs + { + public bool BoolValue { get; private set; } + public int IntValue { get; private set; } + public ushort UShortValue + { + get + { + return (ushort)IntValue; + } + } + public string StringValue { get; private set; } + public eFeedbackEventType Type { get; private set; } + + public FeedbackEventArgs(bool value) + { + BoolValue = value; + Type = eFeedbackEventType.TypeBool; + } + + public FeedbackEventArgs(int value) + { + IntValue = value; + Type = eFeedbackEventType.TypeInt; + } + + public FeedbackEventArgs(string value) + { + StringValue = value; + Type = eFeedbackEventType.TypeString; + } + } + + public enum eFeedbackEventType + { + TypeBool, + TypeInt, + TypeString + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/IntFeedback.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/IntFeedback.cs new file mode 100644 index 00000000..5591cb7f --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/IntFeedback.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +namespace PepperDash.Essentials.Core +{ + public class IntFeedback : Feedback + { + public override int IntValue { get { return _IntValue; } } // ValueFunc.Invoke(); } } + int _IntValue; + public ushort UShortValue { get { return (ushort)_IntValue; } } + + public override eCueType Type { get { return eCueType.Int; } } + + public int TestValue { get; private set; } + + /// + /// Func evaluated on FireUpdate + /// + Func ValueFunc; + List LinkedInputSigs = new List(); + + public IntFeedback(Func valueFunc) + : this(null, valueFunc) + { + } + + public IntFeedback(string key, Func valueFunc) + : base(key) + { + ValueFunc = valueFunc; + } + + //public IntFeedback(Cue cue, Func valueFunc) + // : base(cue) + //{ + // if (cue == null) throw new ArgumentNullException("cue"); + // ValueFunc = valueFunc; + //} + + public override void FireUpdate() + { + var newValue = InTestMode ? TestValue : ValueFunc.Invoke(); + if (newValue != _IntValue) + { + _IntValue = newValue; + LinkedInputSigs.ForEach(s => UpdateSig(s)); + OnOutputChange(newValue); + } + } + + public void LinkInputSig(UShortInputSig sig) + { + LinkedInputSigs.Add(sig); + UpdateSig(sig); + } + + public void UnlinkInputSig(UShortInputSig sig) + { + LinkedInputSigs.Remove(sig); + } + + public override string ToString() + { + return (InTestMode ? "TEST -- " : "") + IntValue.ToString(); + } + + /// + /// Puts this in test mode, sets the test value and fires an update. + /// + /// + public void SetTestValue(int value) + { + TestValue = value; + InTestMode = true; + FireUpdate(); + } + + void UpdateSig(UShortInputSig sig) + { + sig.UShortValue = UShortValue; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/SerialFeedback.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/SerialFeedback.cs new file mode 100644 index 00000000..0e6ed424 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/SerialFeedback.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +namespace PepperDash.Essentials.Core +{ + /// + /// To be used for serial data feedback where the event chain / asynchronicity must be maintained + /// and calculating the value based on a Func when it is needed will not suffice. + /// + public class SerialFeedback : Feedback + { + public override string SerialValue { get { return _SerialValue; } } + string _SerialValue; + + public override eCueType Type { get { return eCueType.Serial; } } + + /// + /// Used in testing. Set/Clear functions + /// + public string TestValue { get; private set; } + + List LinkedInputSigs = new List(); + + public SerialFeedback() + { + } + + public SerialFeedback(string key) + : base(key) + { + } + + public override void FireUpdate() + { + throw new NotImplementedException("This feedback type does not use Funcs"); + } + + public void FireUpdate(string newValue) + { + _SerialValue = newValue; + LinkedInputSigs.ForEach(s => UpdateSig(s, newValue)); + OnOutputChange(newValue); + } + + public void LinkInputSig(StringInputSig sig) + { + LinkedInputSigs.Add(sig); + UpdateSig(sig); + } + + public void UnlinkInputSig(StringInputSig sig) + { + LinkedInputSigs.Remove(sig); + } + + public override string ToString() + { + return (InTestMode ? "TEST -- " : "") + SerialValue; + } + + /// + /// Puts this in test mode, sets the test value and fires an update. + /// + /// + public void SetTestValue(string value) + { + TestValue = value; + InTestMode = true; + FireUpdate(TestValue); + } + + void UpdateSig(StringInputSig sig) + { + sig.StringValue = _SerialValue; + } + + void UpdateSig(StringInputSig sig, string value) + { + sig.StringValue = value; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/StringFeedback.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/StringFeedback.cs new file mode 100644 index 00000000..e4cd7084 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Feedbacks/StringFeedback.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +namespace PepperDash.Essentials.Core +{ + public class StringFeedback : Feedback + { + public override string StringValue { get { return _StringValue; } } // ValueFunc.Invoke(); } } + string _StringValue; + + public override eCueType Type { get { return eCueType.String; } } + + /// + /// Used in testing. Set/Clear functions + /// + public string TestValue { get; private set; } + + /// + /// Evalutated on FireUpdate + /// + public Func ValueFunc { get; private set; } + List LinkedInputSigs = new List(); + + public StringFeedback(Func valueFunc) + : this(null, valueFunc) + { + } + + public StringFeedback(string key, Func valueFunc) + : base(key) + { + ValueFunc = valueFunc; + } + + //public StringFeedback(Cue cue, Func valueFunc) + // : base(cue) + //{ + // if (cue == null) throw new ArgumentNullException("cue"); + // ValueFunc = valueFunc; + + //} + + public override void FireUpdate() + { + var newValue = InTestMode ? TestValue : ValueFunc.Invoke(); + if (newValue != _StringValue) + { + _StringValue = newValue; + LinkedInputSigs.ForEach(s => UpdateSig(s)); + OnOutputChange(newValue); + } + } + + public void LinkInputSig(StringInputSig sig) + { + LinkedInputSigs.Add(sig); + UpdateSig(sig); + } + + public void UnlinkInputSig(StringInputSig sig) + { + LinkedInputSigs.Remove(sig); + } + + public override string ToString() + { + return (InTestMode ? "TEST -- " : "") + StringValue; + } + + /// + /// Puts this in test mode, sets the test value and fires an update. + /// + /// + public void SetTestValue(string value) + { + TestValue = value; + InTestMode = true; + FireUpdate(); + } + + void UpdateSig(StringInputSig sig) + { + sig.StringValue = _StringValue; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Fusion/MOVED FusionSystemController.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Fusion/MOVED FusionSystemController.cs new file mode 100644 index 00000000..8e7e70c6 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Fusion/MOVED FusionSystemController.cs @@ -0,0 +1,377 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; + +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; + +//using Crestron.SimplSharpPro.Fusion; +//using PepperDash.Essentials.Core; + +//using PepperDash.Core; + + +//namespace PepperDash.Essentials.Core.Fusion +//{ +// public class EssentialsHuddleSpaceFusionSystemController : Device +// { +// FusionRoom FusionRoom; +// Room Room; +// Dictionary SourceToFeedbackSigs = new Dictionary(); + +// StatusMonitorCollection ErrorMessageRollUp; + +// public EssentialsHuddleSpaceFusionSystemController(HuddleSpaceRoom room, uint ipId) +// : base(room.Key + "-fusion") +// { +// Room = room; + +// FusionRoom = new FusionRoom(ipId, Global.ControlSystem, room.Name, "awesomeGuid-" + room.Key); +// FusionRoom.Register(); + +// FusionRoom.FusionStateChange += new FusionStateEventHandler(FusionRoom_FusionStateChange); + +// // Room to fusion room +// room.RoomIsOn.LinkInputSig(FusionRoom.SystemPowerOn.InputSig); +// var srcName = FusionRoom.CreateOffsetStringSig(50, "Source - Name", eSigIoMask.InputSigOnly); +// room.CurrentSourceName.LinkInputSig(srcName.InputSig); + +// FusionRoom.SystemPowerOn.OutputSig.UserObject = new Action(b => { if (b) room.RoomOn(null); }); +// FusionRoom.SystemPowerOff.OutputSig.UserObject = new Action(b => { if (b) room.RoomOff(); }); +// // NO!! room.RoomIsOn.LinkComplementInputSig(FusionRoom.SystemPowerOff.InputSig); +// FusionRoom.ErrorMessage.InputSig.StringValue = "3: 7 Errors: This is a really long error message;This is a really long error message;This is a really long error message;This is a really long error message;This is a really long error message;This is a really long error message;This is a really long error message;"; + +// // Sources +// foreach (var src in room.Sources) +// { +// var srcNum = src.Key; +// var pSrc = src.Value as IPresentationSource; +// var keyNum = ExtractNumberFromKey(pSrc.Key); +// if (keyNum == -1) +// { +// Debug.Console(1, this, "WARNING: Cannot link source '{0}' to numbered Fusion attributes", pSrc.Key); +// continue; +// } +// string attrName = null; +// uint attrNum = Convert.ToUInt32(keyNum); +// switch (pSrc.Type) +// { +// case PresentationSourceType.None: +// break; +// case PresentationSourceType.SetTopBox: +// attrName = "Source - TV " + keyNum; +// attrNum += 115; // TV starts at 116 +// break; +// case PresentationSourceType.Dvd: +// attrName = "Source - DVD " + keyNum; +// attrNum += 120; // DVD starts at 121 +// break; +// case PresentationSourceType.PC: +// attrName = "Source - PC " + keyNum; +// attrNum += 110; // PC starts at 111 +// break; +// case PresentationSourceType.Laptop: +// attrName = "Source - Laptop " + keyNum; +// attrNum += 100; // Laptops start at 101 +// break; +// case PresentationSourceType.VCR: +// attrName = "Source - VCR " + keyNum; +// attrNum += 125; // VCRs start at 126 +// break; +// } +// if (attrName == null) +// { +// Debug.Console(1, this, "Source type {0} does not have corresponsing Fusion attribute type, skipping", pSrc.Type); +// continue; +// } +// Debug.Console(2, this, "Creating attribute '{0}' with join {1} for source {2}", attrName, attrNum, pSrc.Key); +// var sigD = FusionRoom.CreateOffsetBoolSig(attrNum, attrName, eSigIoMask.InputOutputSig); +// // Need feedback when this source is selected +// // Event handler, added below, will compare source changes with this sig dict +// SourceToFeedbackSigs.Add(pSrc, sigD.InputSig); + +// // And respond to selection in Fusion +// sigD.OutputSig.UserObject = new Action(b => { if(b) room.SelectSource(pSrc); }); +// } + +// // Attach to all room's devices with monitors. +// //foreach (var dev in DeviceManager.Devices) +// foreach (var dev in DeviceManager.GetDevices()) +// { +// if (!(dev is ICommunicationMonitor)) +// continue; + +// var keyNum = ExtractNumberFromKey(dev.Key); +// if (keyNum == -1) +// { +// Debug.Console(1, this, "WARNING: Cannot link device '{0}' to numbered Fusion monitoring attributes", dev.Key); +// continue; +// } +// string attrName = null; +// uint attrNum = Convert.ToUInt32(keyNum); + +// //if (dev is SmartGraphicsTouchpanelControllerBase) +// //{ +// // if (attrNum > 10) +// // continue; +// // attrName = "Device Ok - Touch Panel " + attrNum; +// // attrNum += 200; +// //} +// //// add xpanel here + +// //else +// if (dev is DisplayBase) +// { +// if (attrNum > 10) +// continue; +// attrName = "Device Ok - Display " + attrNum; +// attrNum += 240; +// } +// //else if (dev is DvdDeviceBase) +// //{ +// // if (attrNum > 5) +// // continue; +// // attrName = "Device Ok - DVD " + attrNum; +// // attrNum += 260; +// //} +// // add set top box + +// // add Cresnet roll-up + +// // add DM-devices roll-up + +// if (attrName != null) +// { +// // Link comm status to sig and update +// var sigD = FusionRoom.CreateOffsetBoolSig(attrNum, attrName, eSigIoMask.InputSigOnly); +// var smd = dev as ICommunicationMonitor; +// sigD.InputSig.BoolValue = smd.CommunicationMonitor.Status == MonitorStatus.IsOk; +// smd.CommunicationMonitor.StatusChange += (o, a) => { sigD.InputSig.BoolValue = a.Status == MonitorStatus.IsOk; }; +// Debug.Console(0, this, "Linking '{0}' communication monitor to Fusion '{1}'", dev.Key, attrName); +// } +// } + +// // Don't think we need to get current status of this as nothing should be alive yet. +// room.PresentationSourceChange += Room_PresentationSourceChange; + +// // these get used in multiple places +// var display = room.Display; +// var dispPowerOnAction = new Action(b => { if (!b) display.PowerOn(); }); +// var dispPowerOffAction = new Action(b => { if (!b) display.PowerOff(); }); + +// // Display to fusion room sigs +// FusionRoom.DisplayPowerOn.OutputSig.UserObject = dispPowerOnAction; +// FusionRoom.DisplayPowerOff.OutputSig.UserObject = dispPowerOffAction; +// display.PowerIsOnFeedback.LinkInputSig(FusionRoom.DisplayPowerOn.InputSig); +// if (display is IDisplayUsage) +// (display as IDisplayUsage).LampHours.LinkInputSig(FusionRoom.DisplayUsage.InputSig); + +// // Roll up ALL device errors +// ErrorMessageRollUp = new StatusMonitorCollection(this); +// foreach (var dev in DeviceManager.GetDevices()) +// { +// var md = dev as ICommunicationMonitor; +// if (md != null) +// { +// ErrorMessageRollUp.AddMonitor(md.CommunicationMonitor); +// Debug.Console(2, this, "Adding '{0}' to room's overall error monitor", md.CommunicationMonitor.Parent.Key); +// } +// } +// ErrorMessageRollUp.Start(); +// FusionRoom.ErrorMessage.InputSig.StringValue = ErrorMessageRollUp.Message; +// ErrorMessageRollUp.StatusChange += (o, a) => { +// FusionRoom.ErrorMessage.InputSig.StringValue = ErrorMessageRollUp.Message; }; + + +// // static assets --------------- testing + +// // test assets --- THESE ARE BOTH WIRED TO AssetUsage somewhere internally. +// var ta1 = FusionRoom.CreateStaticAsset(1, "Test asset 1", "Awesome Asset", "Awesome123"); +// ta1.AssetError.InputSig.StringValue = "This should be error"; + + +// var ta2 = FusionRoom.CreateStaticAsset(2, "Test asset 2", "Awesome Asset", "Awesome1232"); +// ta2.AssetUsage.InputSig.StringValue = "This should be usage"; + + +// // Make a display asset +// var dispAsset = FusionRoom.CreateStaticAsset(3, display.Name, "Display", "awesomeDisplayId" + room.Key); +// dispAsset.PowerOn.OutputSig.UserObject = dispPowerOnAction; +// dispAsset.PowerOff.OutputSig.UserObject = dispPowerOffAction; +// display.PowerIsOnFeedback.LinkInputSig(dispAsset.PowerOn.InputSig); +// // NO!! display.PowerIsOn.LinkComplementInputSig(dispAsset.PowerOff.InputSig); +// // Use extension methods +// dispAsset.TrySetMakeModel(display); +// dispAsset.TryLinkAssetErrorToCommunication(display); + + +// // Make it so! +// FusionRVI.GenerateFileForAllFusionDevices(); +// } + +// /// +// /// Helper to get the number from the end of a device's key string +// /// +// /// -1 if no number matched +// int ExtractNumberFromKey(string key) +// { +// var capture = System.Text.RegularExpressions.Regex.Match(key, @"\D+(\d+)"); +// if (!capture.Success) +// return -1; +// else return Convert.ToInt32(capture.Groups[1].Value); +// } + +// void Room_PresentationSourceChange(object sender, EssentialsRoomSourceChangeEventArgs e) +// { +// if (e.OldSource != null) +// { +// if (SourceToFeedbackSigs.ContainsKey(e.OldSource)) +// SourceToFeedbackSigs[e.OldSource].BoolValue = false; +// } +// if (e.NewSource != null) +// { +// if (SourceToFeedbackSigs.ContainsKey(e.NewSource)) +// SourceToFeedbackSigs[e.NewSource].BoolValue = true; +// } +// } + +// void FusionRoom_FusionStateChange(FusionBase device, FusionStateEventArgs args) +// { + +// // The sig/UO method: Need separate handlers for fixed and user sigs, all flavors, +// // even though they all contain sigs. + +// var sigData = (args.UserConfiguredSigDetail as BooleanSigDataFixedName); +// if (sigData != null) +// { +// var outSig = sigData.OutputSig; +// if (outSig.UserObject is Action) +// (outSig.UserObject as Action).Invoke(outSig.BoolValue); +// else if (outSig.UserObject is Action) +// (outSig.UserObject as Action).Invoke(outSig.UShortValue); +// else if (outSig.UserObject is Action) +// (outSig.UserObject as Action).Invoke(outSig.StringValue); +// return; +// } + +// var attrData = (args.UserConfiguredSigDetail as BooleanSigData); +// if (attrData != null) +// { +// var outSig = attrData.OutputSig; +// if (outSig.UserObject is Action) +// (outSig.UserObject as Action).Invoke(outSig.BoolValue); +// else if (outSig.UserObject is Action) +// (outSig.UserObject as Action).Invoke(outSig.UShortValue); +// else if (outSig.UserObject is Action) +// (outSig.UserObject as Action).Invoke(outSig.StringValue); +// return; +// } + +// } +// } + + +// public static class FusionRoomExtensions +// { +// /// +// /// Creates and returns a fusion attribute. The join number will match the established Simpl +// /// standard of 50+, and will generate a 50+ join in the RVI. It calls +// /// FusionRoom.AddSig with join number - 49 +// /// +// /// The new attribute +// public static BooleanSigData CreateOffsetBoolSig(this FusionRoom fr, uint number, string name, eSigIoMask mask) +// { +// if (number < 50) throw new ArgumentOutOfRangeException("number", "Cannot be less than 50"); +// number -= 49; +// fr.AddSig(eSigType.Bool, number, name, mask); +// return fr.UserDefinedBooleanSigDetails[number]; +// } + +// /// +// /// Creates and returns a fusion attribute. The join number will match the established Simpl +// /// standard of 50+, and will generate a 50+ join in the RVI. It calls +// /// FusionRoom.AddSig with join number - 49 +// /// +// /// The new attribute +// public static UShortSigData CreateOffsetUshortSig(this FusionRoom fr, uint number, string name, eSigIoMask mask) +// { +// if (number < 50) throw new ArgumentOutOfRangeException("number", "Cannot be less than 50"); +// number -= 49; +// fr.AddSig(eSigType.UShort, number, name, mask); +// return fr.UserDefinedUShortSigDetails[number]; +// } + +// /// +// /// Creates and returns a fusion attribute. The join number will match the established Simpl +// /// standard of 50+, and will generate a 50+ join in the RVI. It calls +// /// FusionRoom.AddSig with join number - 49 +// /// +// /// The new attribute +// public static StringSigData CreateOffsetStringSig(this FusionRoom fr, uint number, string name, eSigIoMask mask) +// { +// if (number < 50) throw new ArgumentOutOfRangeException("number", "Cannot be less than 50"); +// number -= 49; +// fr.AddSig(eSigType.String, number, name, mask); +// return fr.UserDefinedStringSigDetails[number]; +// } + +// /// +// /// Creates and returns a static asset +// /// +// /// the new asset +// public static FusionStaticAsset CreateStaticAsset(this FusionRoom fr, uint number, string name, string type, string instanceId) +// { +// fr.AddAsset(eAssetType.StaticAsset, number, name, type, instanceId); +// return fr.UserConfigurableAssetDetails[number].Asset as FusionStaticAsset; +// } +// } + +// //************************************************************************************************ +// /// +// /// Extensions to enhance Fusion room, asset and signal creation. +// /// +// public static class FusionStaticAssetExtensions +// { +// /// +// /// Tries to set a Fusion asset with the make and model of a device. +// /// If the provided Device is IMakeModel, will set the corresponding parameters on the fusion static asset. +// /// Otherwise, does nothing. +// /// +// public static void TrySetMakeModel(this FusionStaticAsset asset, Device device) +// { +// var mm = device as IMakeModel; +// if (mm != null) +// { +// asset.ParamMake.Value = mm.DeviceMake; +// asset.ParamModel.Value = mm.DeviceModel; +// } +// } + +// /// +// /// Tries to attach the AssetError input on a Fusion asset to a Device's +// /// CommunicationMonitor.StatusChange event. Does nothing if the device is not +// /// IStatusMonitor +// /// +// /// +// /// +// public static void TryLinkAssetErrorToCommunication(this FusionStaticAsset asset, Device device) +// { +// if (device is ICommunicationMonitor) +// { +// var monitor = (device as ICommunicationMonitor).CommunicationMonitor; +// monitor.StatusChange += (o, a) => +// { +// // Link connected and error inputs on asset +// asset.Connected.InputSig.BoolValue = a.Status == MonitorStatus.IsOk; +// asset.AssetError.InputSig.StringValue = a.Status.ToString(); +// }; +// // set current value +// asset.Connected.InputSig.BoolValue = monitor.Status == MonitorStatus.IsOk; +// asset.AssetError.InputSig.StringValue = monitor.Status.ToString(); +// } +// } +// } + +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/Global.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/Global.cs new file mode 100644 index 00000000..6ce3446e --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/Global.cs @@ -0,0 +1,60 @@ +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronDataStore; +using Crestron.SimplSharpPro; + +//using PepperDash.Essentials.Core.Http; +using PepperDash.Essentials.License; + + + +namespace PepperDash.Essentials.Core +{ + public static class Global + { + public static CrestronControlSystem ControlSystem { get; set; } + + public static LicenseManager LicenseManager { get; set; } + + /// + /// The file path prefix to the folder containing configuration files + /// + public static string FilePathPrefix { get; private set; } + + /// + /// Returns the directory separator character based on the running OS + /// + public static char DirectorySeparator + { + get + { + return System.IO.Path.DirectorySeparatorChar; + } + } + + /// + /// Wildcarded config file name for global reference + /// + public const string ConfigFileName = "*configurationFile*.json"; + + /// + /// Sets the file path prefix + /// + /// + public static void SetFilePathPrefix(string prefix) + { + FilePathPrefix = prefix; + } + + static Global() + { + // Fire up CrestronDataStoreStatic + var err = CrestronDataStoreStatic.InitCrestronDataStore(); + if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + { + CrestronConsole.PrintLine("Error starting CrestronDataStoreStatic: {0}", err); + return; + } + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/JobTimer.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/JobTimer.cs new file mode 100644 index 00000000..83159c12 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/JobTimer.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + public static class JobTimer + { + static CTimer MinuteTimer; + + static List Items = new List(); + + /// + /// + /// + /// + public static void AddAction(Action act) + { + + } + + /// + /// + /// + /// + /// + public static void AddJobTimerItem(JobTimerItem item) + { + var existing = Items.FirstOrDefault(i => i.Key == item.Key); + if (existing != null) + { + Items.Remove(existing); + } + Items.Add(item); + } + + static void CheckAndRunTimer() + { + if (Items.Count > 0 && MinuteTimer == null) + { + MinuteTimer = new CTimer(o => MinuteTimerCallback(), null, 60000, 60000); + } + } + + static void MinuteTimerCallback() + { + + + } + } + + /// + /// + /// + public class JobTimerItem + { + public string Key { get; private set; } + public Action JobAction { get; private set; } + public eJobTimerCycleTypes CycleType { get; private set; } + /// + /// + /// + public DateTime RunNextAt { get; set; } + + public JobTimerItem(string key, eJobTimerCycleTypes cycle, Action act) + { + + } + } + + public enum eJobTimerCycleTypes + { + RunEveryDay, + RunEveryHour, + RunEveryHalfHour, + RunEveryMinute + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/Scheduler.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/Scheduler.cs new file mode 100644 index 00000000..a7b721ef --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Global/Scheduler.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.Scheduler; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// Global Scheduler for the system + /// + public static class Scheduler + { + private static Dictionary EventGroups = new Dictionary(); + + static Scheduler() + { + CrestronConsole.AddNewConsoleCommand(ClearEventsFromGroup, "ClearAllEvents", "Clears all scheduled events for this group", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(ListAllEventGroups, "ListAllEventGroups", "Lists all the event groups by key", ConsoleAccessLevelEnum.AccessOperator); + } + + /// + /// Clears (deletes) all events from a group + /// + /// + static void ClearEventsFromGroup(string groupName) + { + var group = EventGroups[groupName]; + + if (group != null) + group.ClearAllEvents(); + else + Debug.Console(0, "[Scheduler]: Unable to delete events from group '{0}'. Group not found in EventGroups dictionary.", groupName); + } + + static void ListAllEventGroups(string command) + { + Debug.Console(0, "Event Groups:"); + foreach (var group in EventGroups) + { + Debug.Console(0, "{0}", group.Key); + } + } + + /// + /// Adds the event group to the global list + /// + /// + /// + public static void AddEventGroup(ScheduledEventGroup eventGroup) + { + // Add this group to the global collection + if (!EventGroups.ContainsKey(eventGroup.Name)) + EventGroups.Add(eventGroup.Name, eventGroup); + } + + /// + /// Removes the event group from the global list + /// + /// + public static void RemoveEventGroup(ScheduledEventGroup eventGroup) + { + if(!EventGroups.ContainsKey(eventGroup.Name)) + EventGroups.Remove(eventGroup.Name); + } + } + + public static class SchedulerUtilities + { + /// + /// Checks the day of week in eventTime to see if it matches the weekdays defined in the recurrence enum. + /// + /// + /// + /// + public static bool CheckIfDayOfWeekMatchesRecurrenceDays(DateTime eventTime, ScheduledEventCommon.eWeekDays recurrence) + { + bool isMatch = false; + + var dayOfWeek = eventTime.DayOfWeek; + + Debug.Console(1, "[Scheduler]: eventTime day of week is: {0}", dayOfWeek); + + switch (dayOfWeek) + { + case DayOfWeek.Sunday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Sunday) == ScheduledEventCommon.eWeekDays.Sunday) + isMatch = true; + break; + } + case DayOfWeek.Monday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Monday) == ScheduledEventCommon.eWeekDays.Monday) + isMatch = true; + break; + } + case DayOfWeek.Tuesday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Tuesday) == ScheduledEventCommon.eWeekDays.Tuesday) + isMatch = true; + break; + } + case DayOfWeek.Wednesday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Wednesday) == ScheduledEventCommon.eWeekDays.Wednesday) + isMatch = true; + break; + } + case DayOfWeek.Thursday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Thursday) == ScheduledEventCommon.eWeekDays.Thursday) + isMatch = true; + break; + } + case DayOfWeek.Friday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Friday) == ScheduledEventCommon.eWeekDays.Friday) + isMatch = true; + break; + } + case DayOfWeek.Saturday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Saturday) == ScheduledEventCommon.eWeekDays.Saturday) + isMatch = true; + break; + } + } + + Debug.Console(1, "[Scheduler]: eventTime day of week matches recurrence days: {0}", isMatch); + + return isMatch; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/InUseTracking/IInUseTracking.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/InUseTracking/IInUseTracking.cs new file mode 100644 index 00000000..97a7c7f6 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/InUseTracking/IInUseTracking.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + /// + /// Defines a class that uses an InUseTracker + /// + public interface IInUseTracking + { + InUseTracking InUseTracker { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/InUseTracking/InUseTracking.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/InUseTracking/InUseTracking.cs new file mode 100644 index 00000000..4bf1a551 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/InUseTracking/InUseTracking.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + /// + /// Provides in use tracking. Objects can register with this. InUseFeedback can provide + /// events when usage changes. + /// + public class InUseTracking + { + /// + /// Returns a copied list of all users of this tracker. + /// + public IEnumerable Users { get { return new List(_Users); } } + List _Users = new List(); + + /// + /// Feedback that changes when this goes in/out of use + /// + public BoolFeedback InUseFeedback { get; private set; } + + /// + /// Feedback that changes with the count of users + /// + public IntFeedback InUseCountFeedback { get; private set; } + + public InUseTracking() + { + InUseFeedback = new BoolFeedback(() => _Users.Count > 0); + InUseCountFeedback = new IntFeedback(() => _Users.Count); + } + + /// + /// Add a "user" object to this tracker. A user can be added to this tracker + /// multiple times, provided that the label is different + /// + /// A label to identify the instance of the user. Treated like a "role", etc. + public void AddUser(object objectToAdd, string label) + { + // check if an exact object/label pair exists and ignore if so. No double-registers. + var check = _Users.FirstOrDefault(u => u.Label == label && u.User == objectToAdd); + if (check != null) return; + + var prevCount = _Users.Count; + _Users.Add(new InUseTrackingObject(objectToAdd, label)); + // if this is the first add, fire an update + if (prevCount == 0 && _Users.Count > 0) + InUseFeedback.FireUpdate(); + InUseCountFeedback.FireUpdate(); + } + + /// + /// Remove a user object from this tracking + /// + public void RemoveUser(object objectToRemove, string label) + { + // Find the user object if exists and remove it + var toRemove = _Users.FirstOrDefault(u => u.Label == label && u.User == objectToRemove); + if (toRemove != null) + { + _Users.Remove(toRemove); + if (_Users.Count == 0) + InUseFeedback.FireUpdate(); + InUseCountFeedback.FireUpdate(); + } + } + } + + /// + /// Wrapper for label/object pair representing in-use status. Allows the same object to + /// register for in-use with different roles. + /// + public class InUseTrackingObject + { + public string Label { get; private set; } + public object User { get; private set; } + + public InUseTrackingObject(object user, string label) + { + User = user; + Label = label; + } + } + + //public class InUseEventArgs + //{ + // public int EventType { get; private set; } + // public InUseTracking Tracker { get; private set; } + + // public InUseEventArgs(InUseTracking tracker, int eventType) + // { + // Tracker = tracker; + // EventType = eventType; + // } + //} +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/License/EssentialsLicenseManager.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/License/EssentialsLicenseManager.cs new file mode 100644 index 00000000..c5f2d5e9 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/License/EssentialsLicenseManager.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronDataStore; + +using PepperDash.Essentials.Core; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.License +{ + public abstract class LicenseManager + { + public BoolFeedback LicenseIsValid { get; protected set; } + public StringFeedback LicenseMessage { get; protected set; } + public StringFeedback LicenseLog { get; protected set; } + + protected LicenseManager() + { + CrestronConsole.AddNewConsoleCommand( + s => CrestronConsole.ConsoleCommandResponse(GetStatusString()), + "licensestatus", "shows license and related data", + ConsoleAccessLevelEnum.AccessOperator); + } + + protected abstract string GetStatusString(); + } + + public class MockEssentialsLicenseManager : LicenseManager + { + /// + /// Returns the singleton mock license manager for this app + /// + public static MockEssentialsLicenseManager Manager + { + get + { + if (_Manager == null) + _Manager = new MockEssentialsLicenseManager(); + return _Manager; + } + } + static MockEssentialsLicenseManager _Manager; + + bool IsValid; + + MockEssentialsLicenseManager() : base() + { + LicenseIsValid = new BoolFeedback("LicenseIsValid", + () => { return IsValid; }); + CrestronConsole.AddNewConsoleCommand( + s => SetFromConsole(s.Equals("true", StringComparison.OrdinalIgnoreCase)), + "mocklicense", "true or false for testing", ConsoleAccessLevelEnum.AccessOperator); + + bool valid; + var err = CrestronDataStoreStatic.GetGlobalBoolValue("MockLicense", out valid); + if (err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + SetIsValid(valid); + else if (err == CrestronDataStore.CDS_ERROR.CDS_RECORD_NOT_FOUND) + CrestronDataStoreStatic.SetGlobalBoolValue("MockLicense", false); + else + CrestronConsole.PrintLine("Error restoring Mock License setting: {0}", err); + } + + void SetIsValid(bool isValid) + { + IsValid = isValid; + CrestronDataStoreStatic.SetGlobalBoolValue("MockLicense", isValid); + Debug.Console(0, "Mock License is{0} valid", IsValid ? "" : " not"); + LicenseIsValid.FireUpdate(); + } + + void SetFromConsole(bool isValid) + { + SetIsValid(isValid); + } + + protected override string GetStatusString() + { + return string.Format("License Status: {0}", IsValid ? "Valid" : "Not Valid"); + } + } + + public class EssentialsLicenseManager + { + + } + + public class LicenseCue + { + public static Cue LicenseIsValid = Cue.BoolCue("LicenseIsValid", 15991); + + public static Cue LicenseMessage = Cue.StringCue("LicenseMessage", 15991); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Lighting/Lighting Interfaces.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Lighting/Lighting Interfaces.cs new file mode 100644 index 00000000..6c0eaad3 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Lighting/Lighting Interfaces.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.Lighting +{ + /// + /// Requirements for a device that implements lighting scene control + /// + public interface ILightingScenes + { + event EventHandler LightingSceneChange; + + List LightingScenes { get; } + + void SelectScene(LightingScene scene); + + LightingScene CurrentLightingScene { get; } + + } + + /// + /// Requirements for a device that implements master raise/lower + /// + public interface ILightingMasterRaiseLower + { + void MasterRaise(); + void MasterLower(); + void MasterRaiseLowerStop(); + } + + /// + /// Requiremnts for controlling a lighting load + /// + public interface ILightingLoad + { + void SetLoadLevel(int level); + void Raise(); + void Lower(); + + IntFeedback LoadLevelFeedback { get; } + BoolFeedback LoadIsOnFeedback { get; } + } + + public class LightingSceneChangeEventArgs : EventArgs + { + public LightingScene CurrentLightingScene { get; private set; } + + public LightingSceneChangeEventArgs(LightingScene scene) + { + CurrentLightingScene = scene; + } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Lighting/LightingBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Lighting/LightingBase.cs new file mode 100644 index 00000000..af779c16 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Lighting/LightingBase.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.Lighting +{ + public abstract class LightingBase : Device, ILightingScenes + { + #region ILightingScenes Members + + public event EventHandler LightingSceneChange; + + public List LightingScenes { get; protected set; } + + public LightingScene CurrentLightingScene { get; protected set; } + + public IntFeedback CurrentLightingSceneFeedback { get; protected set; } + + #endregion + + + public LightingBase(string key, string name) + : base(key, name) + { + LightingScenes = new List(); + + CurrentLightingScene = new LightingScene(); + //CurrentLightingSceneFeedback = new IntFeedback(() => { return int.Parse(this.CurrentLightingScene.ID); }); + } + + public abstract void SelectScene(LightingScene scene); + + public void SimulateSceneSelect(string sceneName) + { + Debug.Console(1, this, "Simulating selection of scene '{0}'", sceneName); + + var scene = LightingScenes.FirstOrDefault(s => s.Name.Equals(sceneName)); + + if (scene != null) + { + CurrentLightingScene = scene; + OnLightingSceneChange(); + } + } + + /// + /// Sets the IsActive property on each scene and fires the LightingSceneChange event + /// + protected void OnLightingSceneChange() + { + foreach (var scene in LightingScenes) + { + if (scene == CurrentLightingScene) + scene.IsActive = true; + + else + scene.IsActive = false; + } + + var handler = LightingSceneChange; + if (handler != null) + { + handler(this, new LightingSceneChangeEventArgs(CurrentLightingScene)); + } + } + + } + + public class LightingScene + { + public string Name { get; set; } + public string ID { get; set; } + bool _IsActive; + public bool IsActive + { + get + { + return _IsActive; + } + set + { + _IsActive = value; + IsActiveFeedback.FireUpdate(); + } + } + public BoolFeedback IsActiveFeedback { get; set; } + + public LightingScene() + { + IsActiveFeedback = new BoolFeedback(new Func(() => IsActive)); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/CrestronGenericBaseCommunicationMonitor.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/CrestronGenericBaseCommunicationMonitor.cs new file mode 100644 index 00000000..bd57b70a --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/CrestronGenericBaseCommunicationMonitor.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using System.ComponentModel; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public class CrestronGenericBaseCommunicationMonitor : StatusMonitorBase + { + GenericBase Device; + + public CrestronGenericBaseCommunicationMonitor(IKeyed parent, GenericBase device, long warningTime, long errorTime) + : base(parent, warningTime, errorTime) + { + Device = device; + } + + public override void Start() + { + Device.OnlineStatusChange -= Device_OnlineStatusChange; + Device.OnlineStatusChange += Device_OnlineStatusChange; + GetStatus(); + } + + public override void Stop() + { + Device.OnlineStatusChange -= Device_OnlineStatusChange; + } + + void Device_OnlineStatusChange(GenericBase currentDevice, OnlineOfflineEventArgs args) + { + GetStatus(); + } + + void GetStatus() + { + if (Device.IsOnline) + { + Status = MonitorStatus.IsOk; + StopErrorTimers(); + } + else + StartErrorTimers(); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/GenericCommunicationMonitor.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/GenericCommunicationMonitor.cs new file mode 100644 index 00000000..0d19762f --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/GenericCommunicationMonitor.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using System.ComponentModel; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// Used for monitoring comms that are IBasicCommunication. Will send a poll string and provide an event when + /// statuses change. + /// + public class GenericCommunicationMonitor : StatusMonitorBase + { + public IBasicCommunication Client { get; private set; } + + long PollTime; + CTimer PollTimer; + string PollString; + Action PollAction; + + /// + /// + /// + /// + /// in MS, >= 5000 + /// in MS, >= 5000 + /// in MS, >= 5000 + /// String to send to comm + public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime, + long warningTime, long errorTime, string pollString) : + base(parent, warningTime, errorTime) + { + if (pollTime > warningTime || pollTime > errorTime) + throw new ArgumentException("pollTime must be less than warning or errorTime"); + //if (pollTime < 5000) + // throw new ArgumentException("pollTime cannot be less than 5000 ms"); + + Client = client; + PollTime = pollTime; + PollString = pollString; + } + + /// + /// Poll is a provided action instead of string + /// + /// + /// + /// + /// + /// + /// + public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime, + long warningTime, long errorTime, Action pollAction) : + base(parent, warningTime, errorTime) + { + if (pollTime > warningTime || pollTime > errorTime) + throw new ArgumentException("pollTime must be less than warning or errorTime"); + //if (pollTime < 5000) + // throw new ArgumentException("pollTime cannot be less than 5000 ms"); + + Client = client; + PollTime = pollTime; + PollAction = pollAction; + } + + + /// + /// Build the monitor from a config object + /// + public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, + CommunicationMonitorConfig props) : + this(parent, client, props.PollInterval, props.TimeToWarning, props.TimeToError, props.PollString) + { + } + + public override void Start() + { + Client.BytesReceived += Client_BytesReceived; + Poll(); + PollTimer = new CTimer(o => Poll(), null, PollTime, PollTime); + } + + public override void Stop() + { + Client.BytesReceived -= this.Client_BytesReceived; + PollTimer.Stop(); + PollTimer = null; + StopErrorTimers(); + } + + /// + /// Upon any receipt of data, set everything to ok! + /// + /// + /// + void Client_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e) + { + Status = MonitorStatus.IsOk; + ResetErrorTimers(); + // + } + + void Poll() + { + StartErrorTimers(); + if (Client.IsConnected) + { + //Debug.Console(2, this, "Polling"); + if(PollAction != null) + PollAction.Invoke(); + else + Client.SendText(PollString); + } + else + { + Debug.Console(2, this, "Comm not connected"); + } + } + + /// + /// When the client connects, and we're waiting for it, respond and disconect from event + /// + void OneTimeConnectHandler(object o, EventArgs a) + { + if (Client.IsConnected) + { + //Client.IsConnected -= OneTimeConnectHandler; + Debug.Console(2, this, "Comm connected"); + Poll(); + } + } + } + + + public class CommunicationMonitorConfig + { + public int PollInterval { get; set; } + public int TimeToWarning { get; set; } + public int TimeToError { get; set; } + public string PollString { get; set; } + + public CommunicationMonitorConfig() + { + PollInterval = 30000; + TimeToWarning = 120000; + TimeToError = 300000; + PollString = ""; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/Interfaces and things.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/Interfaces and things.cs new file mode 100644 index 00000000..6e4a847e --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/Interfaces and things.cs @@ -0,0 +1,57 @@ + +using System; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + public interface IStatusMonitor + { + IKeyed Parent { get; } + event EventHandler StatusChange; + MonitorStatus Status { get; } + string Message { get; } + BoolFeedback IsOnlineFeedback { get; set; } + void Start(); + void Stop(); + } + + + /// + /// Represents a class that has a basic communication monitoring + /// + public interface ICommunicationMonitor + { + StatusMonitorBase CommunicationMonitor { get; } + } + + /// + /// StatusUnknown = 0, IsOk = 1, InWarning = 2, InError = 3 + /// + public enum MonitorStatus + { + StatusUnknown = 0, + IsOk = 1, + InWarning = 2, + InError = 3 + } + + public class MonitorStatusChangeEventArgs : EventArgs + { + public MonitorStatus Status { get; private set; } + public string Message { get; private set; } + + public MonitorStatusChangeEventArgs(MonitorStatus status) + { + Status = status; + Message = status == MonitorStatus.IsOk ? "" : status.ToString(); + } + + public MonitorStatusChangeEventArgs(MonitorStatus status, string message) + { + Status = status; + Message = message; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/StatusMonitorBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/StatusMonitorBase.cs new file mode 100644 index 00000000..4abb930f --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/StatusMonitorBase.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using System.ComponentModel; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + public abstract class StatusMonitorBase : IStatusMonitor, IKeyName + { + public event EventHandler StatusChange; + + /// + /// Format returned: "parentdevkey-comMonitor" + /// + public string Key { get { return Parent.Key + "-comMonitor"; } } + + public string Name { get { return "Comm. monitor"; } } + + public IKeyed Parent { get; private set; } + + public BoolFeedback IsOnlineFeedback { get; set; } + + public bool IsOnline; + + public MonitorStatus Status + { + get { return _Status; } + protected set + { + if (value != _Status) + { + _Status = value; + + OnStatusChange(value); + } + } + } + MonitorStatus _Status; + + public string Message + { + get { return _Message; } + set + { + if (value == _Message) return; + _Message = value; + OnStatusChange(Status, value); + + } + } + string _Message; + + long WarningTime; + long ErrorTime; + CTimer WarningTimer; + CTimer ErrorTimer; + + public StatusMonitorBase(IKeyed parent, long warningTime, long errorTime) + { + Parent = parent; + if (warningTime > errorTime) + throw new ArgumentException("warningTime must be less than errorTime"); + if (warningTime < 5000 || errorTime < 5000) + throw new ArgumentException("time values cannot be less that 5000 ms"); + + IsOnlineFeedback = new BoolFeedback(() => { return IsOnline; }); + Status = MonitorStatus.StatusUnknown; + WarningTime = warningTime; + ErrorTime = errorTime; + } + + public abstract void Start(); + public abstract void Stop(); + + protected void OnStatusChange(MonitorStatus status) + { + if (_Status == MonitorStatus.IsOk) + IsOnline = true; + else + IsOnline = false; + IsOnlineFeedback.FireUpdate(); + var handler = StatusChange; + if (handler != null) + handler(this, new MonitorStatusChangeEventArgs(status)); + } + + protected void OnStatusChange(MonitorStatus status, string message) + { + if (_Status == MonitorStatus.IsOk) + IsOnline = true; + else + IsOnline = false; + IsOnlineFeedback.FireUpdate(); + var handler = StatusChange; + if (handler != null) + handler(this, new MonitorStatusChangeEventArgs(status, message)); + } + + protected void StartErrorTimers() + { + if (WarningTimer == null) WarningTimer = new CTimer(o => { Status = MonitorStatus.InWarning; }, WarningTime); + if (ErrorTimer == null) ErrorTimer = new CTimer(o => { Status = MonitorStatus.InError; }, ErrorTime); + } + + protected void StopErrorTimers() + { + if (WarningTimer != null) WarningTimer.Stop(); + if (ErrorTimer != null) ErrorTimer.Stop(); + WarningTimer = null; + ErrorTimer = null; + } + + protected void ResetErrorTimers() + { + if(WarningTimer != null) + WarningTimer.Reset(WarningTime, WarningTime); + if(ErrorTimer != null) + ErrorTimer.Reset(ErrorTime, ErrorTime); + + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/StatusMonitorCollection.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/StatusMonitorCollection.cs new file mode 100644 index 00000000..d9b5b33a --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/StatusMonitorCollection.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using System.ComponentModel; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// + /// + public class StatusMonitorCollection : IStatusMonitor + { + public IKeyed Parent { get; private set; } + + List Monitors = new List(); + + #region IStatusMonitor Members + + public event EventHandler StatusChange; + + public MonitorStatus Status { get; protected set; } + + public string Message { get; private set; } + + public BoolFeedback IsOnlineFeedback { get; set; } + + public StatusMonitorCollection(IKeyed parent) + { + Parent = parent; + } + + public void Start() + { + foreach (var mon in Monitors) + mon.StatusChange += mon_StatusChange; + ProcessStatuses(); + } + + + void ProcessStatuses() + { + var InError = Monitors.Where(m => m.Status == MonitorStatus.InError); + var InWarning = Monitors.Where(m => m.Status == MonitorStatus.InWarning); + var IsOk = Monitors.Where(m => m.Status == MonitorStatus.IsOk); + + + MonitorStatus initialStatus; + string prefix = "0:"; + if (InError.Count() > 0) + { + initialStatus = MonitorStatus.InError; + prefix = "3:"; + } + else if (InWarning.Count() > 0) + { + initialStatus = MonitorStatus.InWarning; + prefix = "2:"; + } + else if (InWarning.Count() > 0) + initialStatus = MonitorStatus.IsOk; + else + initialStatus = MonitorStatus.StatusUnknown; + + // Build the error message string + if (InError.Count() > 0 || InWarning.Count() > 0) + { + StringBuilder sb = new StringBuilder(prefix); + if (InError.Count() > 0) + { + // Do string splits and joins + sb.Append(string.Format("{0} Errors:", InError.Count())); + foreach (var mon in InError) + sb.Append(string.Format("{0}, ", mon.Parent.Key)); + } + if (InWarning.Count() > 0) + { + sb.Append(string.Format("{0} Warnings:", InWarning.Count())); + foreach (var mon in InWarning) + sb.Append(string.Format("{0}, ", mon.Parent.Key)); + } + Message = sb.ToString(); + } + + // Want to fire even if status doesn't change because the message may. + Status = initialStatus; + OnStatusChange(initialStatus, Message); + } + + + void mon_StatusChange(object sender, MonitorStatusChangeEventArgs e) + { + ProcessStatuses(); + } + + public void Stop() + { + throw new NotImplementedException(); + } + + #endregion + + public void AddMonitor(IStatusMonitor monitor) + { + if (!Monitors.Contains(monitor)) + Monitors.Add(monitor); + } + + + protected void OnStatusChange(MonitorStatus status, string message) + { + var handler = StatusChange; + if (handler != null) + handler(this, new MonitorStatusChangeEventArgs(status, message)); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/SystemMonitorController.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/SystemMonitorController.cs new file mode 100644 index 00000000..4cc245e0 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Monitoring/SystemMonitorController.cs @@ -0,0 +1,404 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.Diagnostics; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace PepperDash.Essentials.Core.Monitoring +{ + /// + /// Wrapper for the static SystemMonitor class to extend functionality and provide external access + /// to SystemMonitor via APIs + /// + public class SystemMonitorController : Device + { + public event EventHandler SystemMonitorPropertiesChanged; + + public Dictionary ProgramStatusFeedbackCollection; + + public IntFeedback TimeZoneFeedback { get; set; } + public StringFeedback TimeZoneTextFeedback { get; set; } + + public StringFeedback IOControllerVersionFeedback { get; set; } + public StringFeedback SnmpVersionFeedback { get; set; } + public StringFeedback BACnetAppVersionFeedback { get; set; } + public StringFeedback ControllerVersionFeedback { get; set; } + + public SystemMonitorController(string key) + : base(key) + { + Debug.Console(2, this, "Adding SystemMonitorController."); + + SystemMonitor.ProgramInitialization.ProgramInitializationUnderUserControl = true; + + //CrestronConsole.AddNewConsoleCommand(RefreshSystemMonitorData, "RefreshSystemMonitor", "Refreshes System Monitor Feedbacks", ConsoleAccessLevelEnum.AccessOperator); + + TimeZoneFeedback = new IntFeedback(new Func( () => SystemMonitor.TimeZoneInformation.TimeZoneNumber)); + TimeZoneTextFeedback = new StringFeedback(new Func( () => SystemMonitor.TimeZoneInformation.TimeZoneName)); + + IOControllerVersionFeedback = new StringFeedback(new Func( () => SystemMonitor.VersionInformation.IOPVersion)); + SnmpVersionFeedback = new StringFeedback(new Func( () => SystemMonitor.VersionInformation.SNMPVersion)); + BACnetAppVersionFeedback = new StringFeedback(new Func( () => SystemMonitor.VersionInformation.BACNetVersion)); + ControllerVersionFeedback = new StringFeedback(new Func( () => SystemMonitor.VersionInformation.ControlSystemVersion)); + + //var status = string.Format("System Monitor Status: \r TimeZone: {0}\rTimeZoneText: {1}\rIOControllerVersion: {2}\rSnmpAppVersionFeedback: {3}\rBACnetAppVersionFeedback: {4}\rControllerVersionFeedback: {5}", + // SystemMonitor.TimeZoneInformation.TimeZoneNumber, SystemMonitor.TimeZoneInformation.TimeZoneName, SystemMonitor.VersionInformation.IOPVersion, SystemMonitor.VersionInformation.SNMPVersion, + // SystemMonitor.VersionInformation.BACNetVersion, SystemMonitor.VersionInformation.ControlSystemVersion); + + //Debug.Console(1, this, status); + + ProgramStatusFeedbackCollection = new Dictionary(); + + foreach (var prog in SystemMonitor.ProgramCollection) + { + var program = new ProgramStatusFeedbacks(prog); + ProgramStatusFeedbackCollection.Add(prog.Number, program); + } + + SystemMonitor.ProgramChange += new ProgramStateChangeEventHandler(SystemMonitor_ProgramChange); + SystemMonitor.TimeZoneInformation.TimeZoneChange += new TimeZoneChangeEventHandler(TimeZoneInformation_TimeZoneChange); + } + + /// + /// Gets data in separate thread + /// + /// + void RefreshSystemMonitorData(string command) + { + // this takes a while, launch a new thread + CrestronInvoke.BeginInvoke((o) => + { + TimeZoneFeedback.FireUpdate(); + TimeZoneTextFeedback.FireUpdate(); + IOControllerVersionFeedback.FireUpdate(); + SnmpVersionFeedback.FireUpdate(); + BACnetAppVersionFeedback.FireUpdate(); + ControllerVersionFeedback.FireUpdate(); + + OnSystemMonitorPropertiesChanged(); + } + ); + } + + void OnSystemMonitorPropertiesChanged() + { + var handler = SystemMonitorPropertiesChanged; + if (handler != null) + { + handler(this, new EventArgs()); + } + } + + public override bool CustomActivate() + { + RefreshSystemMonitorData(null); + + return base.CustomActivate(); + } + + //// Sets the time zone + //public void SetTimeZone(int timeZone) + //{ + // SystemMonitor.TimeZoneInformation.TimeZoneNumber = timeZone; + //} + + /// + /// Responds to program change events and triggers the appropriate feedbacks to update + /// + /// + /// + void SystemMonitor_ProgramChange(Program sender, ProgramEventArgs args) + { + Debug.Console(2, this, "Program Change Detected for slot: {0}", sender.Number); + Debug.Console(2, this, "Event Type: {0}", args.EventType); + + var program = ProgramStatusFeedbackCollection[sender.Number]; + + if (args.EventType == eProgramChangeEventType.OperatingState) + { + program.ProgramStartedFeedback.FireUpdate(); + program.ProgramStoppedFeedback.FireUpdate(); + + program.ProgramInfo.OperatingState = args.OperatingState; + + //program.GetProgramInfo(); + + if (args.OperatingState == eProgramOperatingState.Start) + program.GetProgramInfo(); + else + { + program.AggregatedProgramInfoFeedback.FireUpdate(); + program.OnProgramInfoChanged(); + } + } + else if (args.EventType == eProgramChangeEventType.RegistrationState) + { + program.ProgramRegisteredFeedback.FireUpdate(); + program.ProgramUnregisteredFeedback.FireUpdate(); + + program.ProgramInfo.RegistrationState = args.RegistrationState; + + program.GetProgramInfo(); + } + } + + /// + /// Responds to time zone changes and updates the appropriate feedbacks + /// + /// + void TimeZoneInformation_TimeZoneChange(TimeZoneEventArgs args) + { + Debug.Console(2, this, "Time Zone Change Detected."); + TimeZoneFeedback.FireUpdate(); + TimeZoneTextFeedback.FireUpdate(); + + OnSystemMonitorPropertiesChanged(); + } + + + public class ProgramStatusFeedbacks + { + public event EventHandler ProgramInfoChanged; + + public Program Program; + + public ProgramInfo ProgramInfo { get; set; } + + public BoolFeedback ProgramStartedFeedback; + public BoolFeedback ProgramStoppedFeedback; + public BoolFeedback ProgramRegisteredFeedback; + public BoolFeedback ProgramUnregisteredFeedback; + + public StringFeedback ProgramNameFeedback; + public StringFeedback ProgramCompileTimeFeedback; + public StringFeedback CrestronDataBaseVersionFeedback; + // SIMPL windows version + public StringFeedback EnvironmentVersionFeedback; + public StringFeedback AggregatedProgramInfoFeedback; + + public ProgramStatusFeedbacks(Program program) + { + ProgramInfo = new ProgramInfo(program.Number); + + Program = program; + + ProgramInfo.OperatingState = Program.OperatingState; + ProgramInfo.RegistrationState = Program.RegistrationState; + + ProgramStartedFeedback = new BoolFeedback(new Func( () => Program.OperatingState == eProgramOperatingState.Start)); + ProgramStoppedFeedback = new BoolFeedback(new Func( () => Program.OperatingState == eProgramOperatingState.Stop)); + ProgramRegisteredFeedback = new BoolFeedback(new Func( () => Program.RegistrationState == eProgramRegistrationState.Register)); + ProgramUnregisteredFeedback = new BoolFeedback(new Func( () => Program.RegistrationState == eProgramRegistrationState.Unregister)); + + ProgramNameFeedback = new StringFeedback(new Func(() => ProgramInfo.ProgramFile)); + ProgramCompileTimeFeedback = new StringFeedback(new Func(() => ProgramInfo.CompileTime)); + CrestronDataBaseVersionFeedback = new StringFeedback(new Func(() => ProgramInfo.CrestronDB)); + EnvironmentVersionFeedback = new StringFeedback(new Func(() => ProgramInfo.Environment)); + + AggregatedProgramInfoFeedback = new StringFeedback(new Func(() => JsonConvert.SerializeObject(ProgramInfo))); + + GetProgramInfo(); + } + + /// + /// Retrieves information about a running program + /// + public void GetProgramInfo() + { + CrestronInvoke.BeginInvoke((o) => + { + Debug.Console(2, "Attempting to get program info for slot: {0}", Program.Number); + + string response = null; + + var success = CrestronConsole.SendControlSystemCommand(string.Format("progcomments:{0}", Program.Number), ref response); + + if (success) + { + //Debug.Console(2, "Progcomments Response: \r{0}", response); + + if (!response.ToLower().Contains("bad or incomplete")) + { + // Shared properteis + ProgramInfo.ProgramFile = ParseConsoleData(response, "Program File", ": ", "\n"); + ProgramInfo.CompilerRevision = ParseConsoleData(response, "Compiler Rev", ": ", "\n"); + ProgramInfo.CompileTime = ParseConsoleData(response, "Compiled On", ": ", "\n"); + ProgramInfo.Include4Dat = ParseConsoleData(response, "Include4.dat", ": ", "\n"); + + + + if (ProgramInfo.ProgramFile.Contains(".dll")) + { + // SSP Program + ProgramInfo.FriendlyName = ParseConsoleData(response, "Friendly Name", ": ", "\n"); + ProgramInfo.ApplicationName = ParseConsoleData(response, "Application Name", ": ", "\n"); + ProgramInfo.ProgramTool = ParseConsoleData(response, "Program Tool", ": ", "\n"); + ProgramInfo.MinFirmwareVersion = ParseConsoleData(response, "Min Firmware Version", ": ", "\n"); + ProgramInfo.PlugInVersion = ParseConsoleData(response, "PlugInVersion", ": ", "\n"); + } + else if (ProgramInfo.ProgramFile.Contains(".smw")) + { + // SIMPL Windows Program + ProgramInfo.FriendlyName = ParseConsoleData(response, "Friendly Name", ":", "\n"); + ProgramInfo.SystemName = ParseConsoleData(response, "System Name", ": ", "\n"); + ProgramInfo.CrestronDB = ParseConsoleData(response, "CrestronDB", ": ", "\n"); + ProgramInfo.Environment = ParseConsoleData(response, "Source Env", ": ", "\n"); + ProgramInfo.Programmer = ParseConsoleData(response, "Programmer", ": ", "\n"); + + } + //Debug.Console(2, "ProgramInfo: \r{0}", JsonConvert.SerializeObject(ProgramInfo)); + } + else + { + Debug.Console(2, "Bad or incomplete console command response. Initializing ProgramInfo for slot: {0}", Program.Number); + + // Assume no valid program info. Constructing a new object will wipe all properties + ProgramInfo = new ProgramInfo(Program.Number); + + ProgramInfo.OperatingState = Program.OperatingState; + ProgramInfo.RegistrationState = Program.RegistrationState; + } + } + else + { + Debug.Console(2, "Progcomments Attempt Unsuccessful for slot: {0}", Program.Number); + } + + ProgramNameFeedback.FireUpdate(); + ProgramCompileTimeFeedback.FireUpdate(); + CrestronDataBaseVersionFeedback.FireUpdate(); + EnvironmentVersionFeedback.FireUpdate(); + + AggregatedProgramInfoFeedback.FireUpdate(); + + OnProgramInfoChanged(); + }); + } + + public void OnProgramInfoChanged() + { + //Debug.Console(1, "Firing ProgramInfoChanged for slot: {0}", Program.Number); + var handler = ProgramInfoChanged; + if (handler != null) + { + handler(this, new ProgramInfoEventArgs(ProgramInfo)); + } + } + + private string ParseConsoleData(string data, string line, string startString, string endString) + { + string outputData = ""; + + if (data.Length > 0) + { + try + { + + //Debug.Console(2, "ParseConsoleData Data: {0}, Line {1}, startStirng {2}, endString {3}", data, line, startString, endString); + var linePosition = data.IndexOf(line); + var startPosition = data.IndexOf(startString, linePosition) + startString.Length; + var endPosition = data.IndexOf(endString, startPosition); + outputData = data.Substring(startPosition, endPosition - startPosition).Trim(); + //Debug.Console(2, "ParseConsoleData Return: {0}", outputData); + } + catch (Exception e) + { + Debug.Console(1, "Error Parsing Console Data:\r{0}", e); + } + } + + return outputData; + } + } + + } + + /// + /// Class for serializing program slot information + /// + public class ProgramInfo + { + // Shared properties + + [JsonProperty("programNumber")] + public uint ProgramNumber { get; private set; } + + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("operatingState")] + public eProgramOperatingState OperatingState { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("registrationState")] + public eProgramRegistrationState RegistrationState { get; set; } + + [JsonProperty("programFile")] + public string ProgramFile { get; set; } + [JsonProperty("friendlyName")] + public string FriendlyName { get; set; } + [JsonProperty("compilerRevision")] + public string CompilerRevision { get; set; } + [JsonProperty("compileTime")] + public string CompileTime { get; set; } + [JsonProperty("include4Dat")] + public string Include4Dat { get; set; } + + // SIMPL Windows properties + [JsonProperty("systemName")] + public string SystemName { get; set; } + [JsonProperty("crestronDb")] + public string CrestronDB { get; set; } + [JsonProperty("environment")] + public string Environment { get; set; } + [JsonProperty("programmer")] + public string Programmer { get; set; } + + + // SSP Properties + [JsonProperty("applicationName")] + public string ApplicationName { get; set; } + [JsonProperty("programTool")] + public string ProgramTool { get; set; } + [JsonProperty("minFirmwareVersion")] + public string MinFirmwareVersion { get; set; } + [JsonProperty("plugInVersion")] + public string PlugInVersion { get; set; } + + public ProgramInfo(uint number) + { + ProgramNumber = number; + + ProgramFile = ""; + FriendlyName = ""; + CompilerRevision = ""; + CompileTime = ""; + Include4Dat = ""; + + SystemName = ""; + CrestronDB = ""; + Environment = ""; + Programmer = ""; + + ApplicationName = ""; + ProgramTool = ""; + MinFirmwareVersion = ""; + PlugInVersion = ""; + } + } + + public class ProgramInfoEventArgs : EventArgs + { + public ProgramInfo ProgramInfo; + + public ProgramInfoEventArgs(ProgramInfo progInfo) + { + ProgramInfo = progInfo; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/PepperDash_Essentials_Core.csproj b/essentials-framework/Essentials Core/PepperDashEssentialsBase/PepperDash_Essentials_Core.csproj new file mode 100644 index 00000000..39cb2235 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/PepperDash_Essentials_Core.csproj @@ -0,0 +1,265 @@ + + + Release + AnyCPU + 9.0.30729 + 2.0 + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5} + Library + Properties + PepperDash.Essentials.Core + PepperDash_Essentials_Core + {0B4745B0-194B-4BB6-8E21-E9057CA92300};{4D628B5B-2FBC-4AA6-8C16-197242AEB884};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + WindowsCE + E2BECB1F-8C8C-41ba-B736-9BE7D946A398 + 5.0 + SmartDeviceProject1 + v3.5 + Windows CE + + + + + .allowedReferenceRelatedFileExtensions + true + full + false + bin\ + DEBUG;TRACE; + prompt + 4 + 512 + true + true + off + + + .allowedReferenceRelatedFileExtensions + none + true + bin\ + prompt + 4 + 512 + true + true + off + + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.DeviceSupport.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.DM.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.EthernetCommunications.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.Fusion.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.Remotes.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.UI.dll + + + + False + ..\..\references\PepperDash_Core.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpCustomAttributesInterface.dll + False + + + False + ..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpHelperInterface.dll + False + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpNewtonsoft.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpPro.exe + False + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpReflectionInterface.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpTimerEventInterface.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rem S# Pro preparation will execute after these operations + + \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/DevicePresets.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/DevicePresets.cs new file mode 100644 index 00000000..4f79626f --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/DevicePresets.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using PepperDash.Core; + +//using SSMono.IO; + +namespace PepperDash.Essentials.Core.Presets +{ + /// + /// Class that represents the model behind presets display + /// + public class DevicePresetsModel : Device + { + public event EventHandler PresetsLoaded; + + public int PulseTime { get; set; } + public int DigitSpacingMS { get; set; } + public bool PresetsAreLoaded { get; private set; } + + public List PresetsList { get { return _PresetsList.ToList(); } } + List _PresetsList; + public int Count { get { return PresetsList != null ? PresetsList.Count : 0; } } + + public bool UseLocalImageStorage { get; set; } + public string ImagesLocalHostPrefix { get; set; } + public string ImagesPathPrefix { get; set; } + public string ListPathPrefix { get; set; } + + /// + /// The methods on the STB device to call when dialing + /// + Dictionary> DialFunctions; + Action EnterFunction; + + bool DialIsRunning; + string FilePath; + bool InitSuccess; + //SSMono.IO.FileSystemWatcher ListWatcher; + + public DevicePresetsModel(string key, ISetTopBoxNumericKeypad setTopBox, string fileName) + : base(key) + { + PulseTime = 150; + DigitSpacingMS = 150; + + try + { + // Grab the digit functions from the device + // If any fail, the whole thing fails peacefully + DialFunctions = new Dictionary>(10) + { + { '1', setTopBox.Digit1 }, + { '2', setTopBox.Digit2 }, + { '3', setTopBox.Digit3 }, + { '4', setTopBox.Digit4 }, + { '5', setTopBox.Digit5 }, + { '6', setTopBox.Digit6 }, + { '7', setTopBox.Digit7 }, + { '8', setTopBox.Digit8 }, + { '9', setTopBox.Digit9 }, + { '0', setTopBox.Digit0 }, + { '-', setTopBox.Dash } + }; + } + catch + { + Debug.Console(0, "DevicePresets '{0}', not attached to INumericKeypad device. Ignoring", key); + DialFunctions = null; + return; + } + + EnterFunction = setTopBox.KeypadEnter; + + UseLocalImageStorage = true; + + ImagesLocalHostPrefix = "http://" + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS,0); + ImagesPathPrefix = @"/presets/images.zip/"; + ListPathPrefix = @"/html/presets/lists/"; + + SetFileName(fileName); + + //ListWatcher = new FileSystemWatcher(@"\HTML\presets\lists"); + //ListWatcher.NotifyFilter = NotifyFilters.LastWrite; + //ListWatcher.EnableRaisingEvents = true; + //ListWatcher.Changed += ListWatcher_Changed; + InitSuccess = true; + } + + + public void SetFileName(string path) + { + FilePath = ListPathPrefix + path; + LoadChannels(); + } + + public void LoadChannels() + { + PresetsAreLoaded = false; + try + { + var pl = JsonConvert.DeserializeObject(Crestron.SimplSharp.CrestronIO.File.ReadToEnd(FilePath, Encoding.ASCII)); + Name = pl.Name; + _PresetsList = pl.Channels; + } + catch (Exception e) + { + Debug.Console(2, this, "LoadChannels: Error reading presets file. These presets will be empty:\r '{0}'\r Error:{1}", FilePath, e.Message); + // Just save a default empty list + _PresetsList = new List(); + } + PresetsAreLoaded = true; + + var handler = PresetsLoaded; + if (handler != null) + handler(this, EventArgs.Empty); + } + + public void Dial(int presetNum) + { + if (presetNum <= _PresetsList.Count) + Dial(_PresetsList[presetNum - 1].Channel); + } + + public void Dial(string chanNum) + { + if (DialIsRunning || !InitSuccess) return; + if (DialFunctions == null) + { + Debug.Console(1, "DevicePresets '{0}', not attached to keypad device. Ignoring channel", Key); + return; + } + + DialIsRunning = true; + CrestronInvoke.BeginInvoke(o => + { + foreach (var c in chanNum.ToCharArray()) + { + if (DialFunctions.ContainsKey(c)) + Pulse(DialFunctions[c]); + CrestronEnvironment.Sleep(DigitSpacingMS); + } + + if (EnterFunction != null) + Pulse(EnterFunction); + DialIsRunning = false; + }); + } + + void Pulse(Action act) + { + act(true); + CrestronEnvironment.Sleep(PulseTime); + act(false); + } + + /// + /// Event handler for filesystem watcher. When directory changes, this is called + /// + //void ListWatcher_Changed(object sender, FileSystemEventArgs e) + //{ + // Debug.Console(1, this, "folder modified: {0}", e.FullPath); + // if (e.FullPath.Equals(FilePath, StringComparison.OrdinalIgnoreCase)) + // { + // Debug.Console(1, this, "File changed: {0}", e.ChangeType); + // LoadChannels(); + // } + //} + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/DevicePresetsView.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/DevicePresetsView.cs new file mode 100644 index 00000000..a43a7a2c --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/DevicePresetsView.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +namespace PepperDash.Essentials.Core.Presets +{ + public class DevicePresetsView + { + public bool ShowNumbers { get; set; } + public bool ShowName { get; set; } + public bool ShowIcon { get; set; } + + public SubpageReferenceList SRL { get; private set; } + public DevicePresetsModel Model { get; private set; } + + public DevicePresetsView(BasicTriListWithSmartObject tl, DevicePresetsModel model) + { + if (model == null) + { + throw new ArgumentNullException("model", "DevicePresetsView Cannot be instantiated with null model"); + } + ShowIcon = true; + ShowName = true; + + Model = model; + + SRL = new SubpageReferenceList(tl, 10012, 3, 0, 4); + Model.PresetsLoaded += new EventHandler(Model_PresetsLoaded); + } + + public void Attach() + { + if (Model.PresetsAreLoaded) + { + uint index = 1; + foreach (var p in Model.PresetsList) + { + SRL.AddItem(new PresetsListSubpageReferenceListItem(p, index, SRL, this)); + index++; + } + SRL.Count = (ushort)Model.PresetsList.Count; + } + } + + public void Detach() + { + SRL.Clear(); + } + + void Model_PresetsLoaded(object sender, EventArgs e) + { + Detach(); + Attach(); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/Interfaces.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/Interfaces.cs new file mode 100644 index 00000000..95773c58 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/Interfaces.cs @@ -0,0 +1,22 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; + +//namespace PepperDash.Essentials.Core +//{ +// public interface IPresetsFileChanged : IKeyed +// { +// public event EventHandler PresetsFileChanged; +// } + +// public class PresetsFileChangeEventArgs : EventArgs +// { +// public string FilePath { get; private set; } +// public PresetsFileChangeEventArgs(string filePath) +// { +// FilePath = filePath; +// } +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/PresetChannel.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/PresetChannel.cs new file mode 100644 index 00000000..b9650e60 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/PresetChannel.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Newtonsoft.Json; + +namespace PepperDash.Essentials.Core.Presets +{ + public class PresetChannel + { + + [JsonProperty(Required = Required.Always)] + public string Name { get; set; } + [JsonProperty(Required = Required.Always)] + public string IconUrl { get; set; } + [JsonProperty(Required = Required.Always)] + public string Channel { get; set; } + } + + public class PresetsList + { + [JsonProperty(Required=Required.Always)] + public string Name { get; set; } + [JsonProperty(Required = Required.Always)] + public List Channels { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/PresetsListSubpageReferenceListItem.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/PresetsListSubpageReferenceListItem.cs new file mode 100644 index 00000000..78a9d1bf --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Presets/PresetsListSubpageReferenceListItem.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core.Presets +{ + public class PresetsListSubpageReferenceListItem : SubpageReferenceListItem + { + DevicePresetsView View; + PresetChannel Channel; + + public PresetsListSubpageReferenceListItem(PresetChannel chan, uint index, + SubpageReferenceList owner, DevicePresetsView view) + : base(index, owner) + { + View = view; + Channel = chan; + owner.GetBoolFeedbackSig(index, 1).UserObject = new Action(b => { if (!b) view.Model.Dial((int)index); }); + Refresh(); + } + + public override void Clear() + { + Owner.GetBoolFeedbackSig(Index, 1).UserObject = null; + Owner.StringInputSig(Index, 1).StringValue = ""; + Owner.StringInputSig(Index, 2).StringValue = ""; + Owner.StringInputSig(Index, 3).StringValue = ""; + } + + public override void Refresh() + { + var name = View.ShowName ? Channel.Name : ""; + Owner.StringInputSig(Index, 1).StringValue = name; + var chan = View.ShowNumbers ? Channel.Channel : ""; + Owner.StringInputSig(Index, 2).StringValue = chan; + var url = View.Model.ImagesLocalHostPrefix + View.Model.ImagesPathPrefix + Channel.IconUrl; + Debug.Console(2, "icon url={0}", url); + var icon = View.ShowIcon ? url : ""; + Owner.StringInputSig(Index, 3).StringValue = icon; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Properties/AssemblyInfo.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..187d57a6 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; + +[assembly: AssemblyTitle("PepperDashEssentialsBase")] +[assembly: AssemblyCompany("PepperDash Technology Corp")] +[assembly: AssemblyProduct("PepperDashEssentialsBase")] +[assembly: AssemblyCopyright("Copyright © Ppperdash 2019")] +[assembly: AssemblyVersion("1.4.2.*")] \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Properties/ControlSystem.cfg b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Properties/ControlSystem.cfg new file mode 100644 index 00000000..e69de29b diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/REMOVE SigId.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/REMOVE SigId.cs new file mode 100644 index 00000000..884f1601 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/REMOVE SigId.cs @@ -0,0 +1,37 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharpPro; + +//namespace PepperDash.Essentials.Core +//{ +// public class SigId +// { +// public uint Number { get; private set; } +// public eSigType Type { get; private set; } + +// public SigId(eSigType type, uint number) +// { +// Type = type; +// Number = number; +// } + +// public override bool Equals(object id) +// { +// if (id is SigId) +// { +// var sigId = id as SigId; +// return this.Number == sigId.Number && this.Type == sigId.Type; +// } +// else +// return base.Equals(id); +// } + +// public override int GetHashCode() +// { +// return base.GetHashCode(); +// } +// } + +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/ActionIncrementer.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/ActionIncrementer.cs new file mode 100644 index 00000000..f63d57bf --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/ActionIncrementer.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// An incrementer that can use the values of some other object/primitive value to do its thing. + /// It uses an Action to set the value and a Func to get the value from whatever this is + /// attached to. + /// + public class ActionIncrementer + { + public int ChangeAmount { get; set; } + public int MaxValue { get; set; } + public int MinValue { get; set; } + public uint RepeatDelay { get; set; } + public uint RepeatTime { get; set; } + + Action SetAction; + Func GetFunc; + CTimer Timer; + + /// + /// + /// + /// + /// + /// + /// + /// + /// Action that will be called when this needs to set the destination value + /// Func that is called to get the current value + public ActionIncrementer(int changeAmount, int minValue, int maxValue, uint repeatDelay, uint repeatTime, Action setAction, Func getFunc) + { + SetAction = setAction; + GetFunc = getFunc; + ChangeAmount = changeAmount; + MaxValue = maxValue; + MinValue = minValue; + RepeatDelay = repeatDelay; + RepeatTime = repeatTime; + } + + /// + /// Starts incrementing cycle + /// + public void StartUp() + { + if (Timer != null) return; + Go(ChangeAmount); + } + + /// + /// Starts decrementing cycle + /// + public void StartDown() + { + if (Timer != null) return; + Go(-ChangeAmount); + } + + /// + /// Stops the repeat + /// + public void Stop() + { + if (Timer != null) + Timer.Stop(); + Timer = null; + } + + /// + /// Helper that does the work of setting new level, and starting repeat loop, checking against bounds first. + /// + /// + void Go(int change) + { + int currentLevel = GetFunc(); + // Fire once then pause + int newLevel = currentLevel + change; + bool atLimit = CheckLevel(newLevel, out newLevel); + SetAction(newLevel); + + if (atLimit) // Don't go past end + Stop(); + else if (Timer == null) // Only enter the timer if it's not already running + Timer = new CTimer(o => { Go(change); }, null, RepeatDelay, RepeatTime); + } + + /// + /// Helper to check a new level against min/max. Returns revised level if new level + /// will go out of bounds + /// + /// The level to check against bounds + /// Revised level if bounds are exceeded. Min or max + /// true if new level is at or past bounds + bool CheckLevel(int levelIn, out int levelOut) + { + bool isAtLimit = false; + if (levelIn > MaxValue) + { + levelOut = MaxValue; + isAtLimit = true; + } + else if (levelIn < MinValue) + { + levelOut = MinValue; + isAtLimit = true; + } + else + levelOut = levelIn; + return isAtLimit; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/NumericalHelpers.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/NumericalHelpers.cs new file mode 100644 index 00000000..50c12ddc --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/NumericalHelpers.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core +{ + public class NumericalHelpers + { + /// + /// Scales a value + /// + /// + /// + /// + /// + /// + /// + public static double Scale(double input, double inMin, double inMax, double outMin, double outMax) + { + //Debug.Console(2, this, "Scaling (double) input '{0}' with min '{1}'/max '{2}' to output range min '{3}'/max '{4}'", input, inMin, inMax, outMin, outMax); + + double inputRange = inMax - inMin; + + if (inputRange <= 0) + { + throw new ArithmeticException(string.Format("Invalid Input Range '{0}' for Scaling. Min '{1}' Max '{2}'.", inputRange, inMin, inMax)); + } + + double outputRange = outMax - outMin; + + var output = (((input - inMin) * outputRange) / inputRange) + outMin; + + // Debug.Console(2, this, "Scaled output '{0}'", output); + + return output; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/UshortSigIncrementer.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/UshortSigIncrementer.cs new file mode 100644 index 00000000..1a6f3c06 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Ramps and Increments/UshortSigIncrementer.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// Attaches to UShortInputSig and does incremental ramping of the signal + /// + public class UshortSigIncrementer + { + UShortInputSig TheSig; + public ushort ChangeAmount { get; set; } + public int MaxValue { get; set; } + public int MinValue { get; set; } + public uint RepeatDelay { get; set; } + public uint RepeatTime { get; set; } + bool SignedMode; + CTimer Timer; + + public UshortSigIncrementer(UShortInputSig sig, ushort changeAmount, int minValue, int maxValue, uint repeatDelay, uint repeatTime) + { + TheSig = sig; + ChangeAmount = changeAmount; + MaxValue = maxValue; + MinValue = minValue; + if (MinValue < 0 || MaxValue < 0) SignedMode = true; + RepeatDelay = repeatDelay; + RepeatTime = repeatTime; + if (SignedMode && (MinValue < -32768 || MaxValue > 32767)) + Debug.Console(1, "UshortSigIncrementer has signed values that exceed range of -32768, 32767"); + } + + public void StartUp() + { + if (Timer != null) return; + Go(ChangeAmount); + } + + public void StartDown() + { + if (Timer != null) return; + Go(-ChangeAmount); + } + + void Go(int change) + { + int level; + if (SignedMode) level = TheSig.ShortValue; + else level = TheSig.UShortValue; + + // Fire once then pause + int newLevel = level + change; + bool atLimit = CheckLevel(newLevel, out newLevel); + SetValue((ushort)newLevel); + + + if (atLimit) // Don't go past end + Stop(); + else if (Timer == null) // Only enter the timer if it's not already running + Timer = new CTimer(o => { Go(change); }, null, RepeatDelay, RepeatTime); + } + + bool CheckLevel(int levelIn, out int levelOut) + { + bool IsAtLimit = false; + if (levelIn > MaxValue) + { + levelOut = MaxValue; + IsAtLimit = true; + } + else if (levelIn < MinValue) + { + levelOut = MinValue; + IsAtLimit = true; + } + else + levelOut = levelIn; + return IsAtLimit; + } + + public void Stop() + { + if (Timer != null) + Timer.Stop(); + Timer = null; + } + + void SetValue(ushort value) + { + //CrestronConsole.PrintLine("Increment level:{0} / {1}", value, (short)value); + TheSig.UShortValue = value; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/SSMonoIOLibrary.clz b/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/SSMonoIOLibrary.clz new file mode 100644 index 0000000000000000000000000000000000000000..1166c23e2d797c2f9149f39122fcb5a9ddfd04a6 GIT binary patch literal 657260 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 xrCiP#PdT{R6;l8$Lp*M2hT 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%0AL3i5$}~`=zDIY-V(Z1Ie>=&-S};qj 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

JhY$YqrXFYEt~cMMf;D7qrFhH-;js) z&KRjywAZp|FaE2v@9@z+b7*oObZ7Jd`%|1gm>}A3apuC1{MzF8x~F=TOFoUD zSDmFoV*pG*v%jm}r(4~aiKq9my^O=U3kSUCoi8hfr>B2GK3<=VYqoZqX94x!$CbxS z*f$5R`G2zS6`n-WNRzV%OA`kZK9^#?Rx2*7N$^o9_?N2!I4#=eV|!cS|F(xeuNLi^ zD3)P7yWDGz$qI(3G1qCG|3SGMn@Q+ABb3#@#=JzHT9IgJOcSGUzgex@vuFX^y{)DDZ%P}!rMu0c@8CdBf`rvj zDO0v;3F9vy4x@;Vm~@Y6>26cWDpYlw{5T;>Od9T318S_tAr3JKaKTOw;>B7l%m(cway6y}zQ&mx!O;lEEVA3EmrY{W`|WK+b#(1n-m^ z`dl{WHJYFXqjKlswjS8H($?9cy}lz&1<$GFA5|S4P4e4DV6C_=d5cy$J&iav zDjuSgwUXy#c0^{gsVHhKKphyVoU0j41j*i&9^I*;mP_t~k`|iO%n5Dyz84KGK)Zs{ z_x@OOtYT4XkhhUex`@+foq>1+Mw`zGi(2pQkPtFAF0(;sk7EPsV?=EPkBagk*x(i+T&58R*XQ$&?^Jk`7P@{7#S9;J6* z?vRM3SDq=_^@P51q47<`&*0GS>L54chBINMgQQ~HTh!7~+ZavmKb825LOFaipvjm` zudCD+w})(;;B5?Sf)Y~2FgD=ZSD)bHMNzqVHL4jeqvVQa`MeF!7a%M*%H^*{`2o}? z>GP;vz8d1XY)2h$4MqQ>aoL;$bnd+4~-AB#32%i@gwRXiBUi!o( z?uV|TRss-jqPnB;aL09W2(tMzYHi={_P2~sPp@=(xouuD$z@*LF29;QoPRyJ9a0Dq zf6^!ny^)-ZZ*p1KIm;hSidr{s_r@ZvHpITzI?G@1ippl&9$A<_!xWX@ z#K~yoP);6$a!-7jZzQS_&lX_nf0RT-H=9qR z)_=ap8uju!l!wX*fU>vXvsQom()sF9)Ozd-d{WFE8T=^EexGt15N+gQH{!j6(3A#F zxLkhO=rw4>9|2sPlg~i8M|&LGZ`7I(d~`WdY?7O?Nx#A8qUt#F5R|@G@+r*&AgDMz zP%}GFyIRBcQF@*q7;uP`{bA8eJat9LJCh)D4seC(3rc;)J$J`I%=sQml%~#u>3s+b z%p2JCJ%--Ml=!ii;UWcCo-y7 zWGGP^0ZV|3;X^s$+MCl|4mB5{a-Pa@P^rl$)8seik ziv!us^5gDQv|GesW87~i=7uSB{gx>HnZ^Nb&UZwxPycGHbF;KZ#6)1sAx2=k(jJ3t z-&D%t%ktzxU7kw7iZhc^9!;!^%q;Tr2*45dL54eXM4EL3eHU&RHC=dW)=*aFHr9k?&@ATPH*zDp~k>2G7XlSVe6dC5z#pQIu_tz z4kLL)CkC^n6}Lxk&tR__=B}t@uBhZK7SmyC6ur8wn*PwRy--0j0=_k74FS8xH`mq2)XI3E2iE!rYEcY) zz3LQbHuw-V0@IX?R=T<)SJZm%37z$sE2*Y&^WBC_R}Go&HAJliuQdlN&md@}tI3VG zMxG`62*)h8zO0sc^JSPBdkl5oOjsiiDec~+S~LEH@=c~lk325W9?9wgQ$)_`c5vRR zr}Px_;n&3de-pd#I`M1b6%qJ9#9aw^9L1IH=^l-wnUQQES(gvV*qA{*@@>=)j$^`2LI6X+fk1Ew$8se$WDRV>a(MlH5CTX9mUG#K zZ-ML*TgTS_-m5-lM)C;h`{M6wYr3nt`qisfuU@^YO8mU-CpqWFF&Tx4mcvBLpkvT3 z&IsEi^o*D}t9gSys_hl$0S|wc8Sea69h3`wk+lH=i!p%}6M+>IfpxEsOUEXtj+Jaw z>oRqW&QN22Y3`l$tgftkLw%E2`<>GQ7W{DY+`+6t+PV%}wr64!0hQdAnx;}s-$xsBFEM6T$sT)IemsUW9w(`%kQHz)FH&DZ1b|r@!L>LX7_@7( zLP?>IrF)4J3AyMj>O8+te0D)AI8>B&U87#A_moH{3i3|$vsj-irNT)9vRCtLJrH%k z>9MsMlL%o4$z85keRrd!=46z06wr?H*xIQ=M)8hEy87fw=OvKtcZ75`-5#-3(=GGc zTXOMjX4C7d!e2qBn&1BJ0Q`2PV0rb>$aU>7`R(B3S`_5BwJyiixX$Lcp@R8s9z5po z+fhQJ&j_CdzeP@ellUz#D{u(EbtO$78(38+Uc0trQEwrr?b;lq-gZG^yAO#0e(Mro zHoukeA-AB}%w+}ppL(RaS994n0yZX+7_<28g6o)WA-`Q}WW~ozz9ZPsQ2jLEl+@PA zli7)x@*T0N8{z3@RR+J^ZbGFSlfMJT%!`Ni4XUN8%>R~1zA88VTYEAT{(6Xz?v zYgSuf+OSfr>%4dZYDNJPkMgdnfoe|vZ;~=arJ)hnW_1xi<>2cpC$4lgS zuRwmU2RUcNgnkt(8N7XJg)d;0Ty1c0^sjwUD`sLW`kIjtjXUi&nXf-J6IH6DX4Y8} zjj+jHEO7-JX{^WNy_r079k`*|&q`FxX;hAO`Pj@x$sw@-m40icD(ghli#pS2`$B#) zYh|p=bdN$r)I7*Y*s5ZrxgqZs@1nEyQ4}shUJ$Z=!AI5%N1_iS(+fOD81lr_x`NK% z892Ygm-+MxL(JdgAM>voFaI|m9DmO?_KiRr`wPbDfNfZr{e|Lm;IZxT!GV09RX!e) zoxkrGgL5u&{yw~EvDS2+tZJ`;k7Q7wmZV<)iuSM&?8E)p2Uu+n9$+;boc8+SN|#G* z1rJRIDojqvm02nAAPPwoA>ciE<5>awS>HUX%J^{xzgVny_Cdw;27KD42%bo<91UPI zX1^w&G|CG32ix#{pS6}T__et}><}P!wz7+=%Pt7ck>Jw>V?zD`f@bCo=u&DIbO z+UdgcwC9dbpAT=h8Cf!uM4)l?6bE?VIKB=Yn{TbPj(9n_7J%Fvt5xf^YT@AMZw~2gj{O;0zD{dMnY5@H#M;Z>@oZ#^@SURXFqKpETj3Tr;|n{_c)Gi_8(oC14%_)?K6Qo z9DWumEl>wOFutseG*amKj(OlP(`KoAX5=QE?`V!#o1?SagiHZZ__nZ5?iPLoKB)I0 z+(2@La3Zw_>1?5sPOPOV2ip)THzTRpNc9^@9j^W?bAjA01GYVjg~#k0^JQ%+sILRg z9W!lt`1>LHY88sV1GYIjyG`H^3clO5zorj=z}q>7Ui0um@=WPA@VAKZmpZDR{JZAk zPimg}e{_gzD{Qva`+~2x2^P2Y=j6PSq1%)dOKp*t5y55%Z1wsThVfk)-!(MG1mj9+ z?A06?X?Z9B&}#+IYqK17WA^2_XJ1;BW8&vN`$BfQVB*O@=acFzLbx8jRc1e2Qrkvn2~x&$d%K2( zgJUQmyL~k;a>?@~bvWD)xSUh3uccW-YrupHWx{(xdSuGhGS! zZYfi1xmS=njzCa-IxYL#0sIX3Z7V~qg)?Z*EXn{Bvpk$RsFe1N_^er-snYAcX{;FZ zJK4O^7fU~C;O~?d@rP>UL-50|&|}2fx6}8;lvqK1NI9XA#qP2 z?y3rzs>Yeci4|Di2aIlnoO*EQ&MS5vq|9BGA(SJVlmwnowo- zGm+-05-jqB4Yho}&q(;jPrGmgp2>lVgc4zz;Mv^`@s1Nt3FX(v*T%YyE=QXmd~qUJ zyC4@KKs}Q5Ov`x`jRM5K!hnB=HYSy+Ic~V8J!rp}q_O!Cqk--5N#p+4jBnt#pVrNE zt&e)gfqIX&^U#|MP@@3U=mBJ=s|lwyOzUHT+9gcuW4waYB2DXO4N8Lp@)avHhH0>d zX|Pskuuf>Oo@st8)BHG(=FcwhdpY?dst^3V@(6#hMn0H_w#&ncxQh~JiHuR~JIBmd zUS5d%b!QRB*hy<|&#%Xar2F z58;?;IeHpqg8vHvxrXt%`aMpL1A&-~^fkNH@4u*W__ISD1Muz)aZZKzOcr(Ee0iGs zjTHJRm!$M`Bdn$C^mL19@6uHWzG~eLp-B9Z=_u-XA#EuuagOMRcWQQ~giUZY7gyYR zyaID2V<`a52Z1Y{J~{*4bos5nP8 zP)z9vDBfN>QQm~IGqw@f@AH=^vWtoOu;awW>1q_SmhnfPHYKpBn#FE-O4GcFWQ z_(SM0Y*koFit@@L?e!Z+uQG~t<)aB5jqpKle}C5}z=b#kzp5UTmQbRLLn^~#+F*du zj>Y73za6t(-1hIKQ<(u$D-IBHoz78j1JGO%t|+Pq`%*_#Mzp`|w|hM_yU8P3a5%5_ zxIGrl;IN-!N~^)W7SH`@D1$wsm^^N|Onh@I;TPIb5ia438I|EuN|{j+rkoiHLE_N; zaZi8$n2MryI-o~ftZ@`Ijj#5WeXhp?)5}e<2kzG5`Mw>m;M`}E0|tpfXdl&0s<8Wn z%3{WUicGX^J`H4^b9QODs3^{Fzpag`6(ic?`fRHuqq2fyWzqYO`#BE3I5|$Hjr}%) z3-9o#$=(xK84P3oZ005i?#rl#9#hu|2K%9!@_dha77cR_^6WLk?aMsr%RE2d)N4Yc zZ(W(>N<|Zh9W?MtaWF8{4zA}H&c=IHQb%OVM?=mvGoE>P1M~1k;o%c`tvHz1ib=Wh z(U5ZuO&It)YOOe!*N91j%17rOP(C`(m5&yPd{h;+8~n&?3@!xw0BX#3MI)pegJ~LD zkLB(#Xh`_NE5)z9o50Qj)PMvSCZLH(sJWiS2Mw9lbm}yKq)2P+(PYS|cm1$yH zJ(RnV;BFj7w26LPso3SO{QVd&ls`Uqh6T@AL|>HL5h1zbXCZds+#KRSj6G_gz7ZxA z8Ly+V#py%-%aqgwst$t8Ae+Ek(P#0|A>dZ!kXOQdMBYj2`BUn?OZ|Q!#7B=@h9IkS zLL-YD$|>R}#;Z?+Y|X{3Z&+I1h&cZM;o*2l6QlUSBHDv{jZny*cb}r*y-!TNbVf*X z;VX+-Aw>~5Pgz>It>hnFM^4H;v*}a2l^zOkZVcnS7rU0%n{GhuxPmgawkL$R?PQ20TN+A*F{@w$R%e*Ulb>vdSNuz}&9!UVs5)O^@ zUd#RIhL8B~5krzH*D)kq;tx$3%bzU4C%;g?{o#ZxK1N9H$Qg!nK?OBj;dzoMdbLy% zah^~QN_=ry(BZMb8F6mwfmx3VvjHz5J{s4@gm$9YFI^hQOFKR^Nbp)UzdRQ&PYR;m z7tqN~azCX;+H0;Y*s>Rp(mg&9jZzuryNWQ!qwqWR9)9^VC4Nu=eeRYG z$9)SUpA74~wU5GuiaMT$&{vdi3GB|Y3CtIT*I{8^xWPm*e>vX8dwzD9^@&SFPJzwt z5Y?3xMP8gy@O*@?8UUuSojzi1j?Qiq^ABfr-0!ztid_`~G4Qa`|Gk&R4DfP3 zPZ$1-_~FIu$8Ez37j-^OHZ>|TBdMKrzUVtPGF)RDI*(qqe`}#Jap9uG<$6zdtyEL< zeKzlp=J21nH2c}{aVC5nXW&8S82p?`EEoS@{LiqymXWa?_qC}C_jhxR9qZV)FZzQ%eN4XP!8o}5SzvL%(yL&LxzzRK(FEaRTrLX^1vR(W+-caIv>OR$s z&)bzCZAafA$TIQ2-wucuveH_58Io?J+8nrMCR?e6ik@@4xH!+{tCmvxd(z zX8PvpSMA%Z5olxfNbro=!{=RdMgopGp1xvfaIDMqSf`3%)pA>YO9uR{0}nXA3HT7F zkT-w%gb2lsAeRnA6Xevt&|ukPiU&)h_%iLakK>lDR7t(|;kAEfFL$BaEzs@u`A6`S zxryIQHa8k{{{#CGZe7|U&nwQ|?ip|I8Ri(x9f>iTJA7I>d?esFJeO92v(ra2(Sd1Z z09uJ^T6s~^3XhwqX#|IP{zZIhki!xA|4;R3Bq_AD*IuD%Bt}f!M=w0`zp>Z3kUb`l zJ?5hqrm8JMFN=gS5UXJSBG_U52_G4;hx#!=GP*|d)Ap~1`^1mQ5$6W=KOYJ(96sv! zXyFbc>rC+0oK<+Xv`U&@?d6YNLf!=o@L7O;)JiVP3J;9h4<5if^x7Qn&@w?cohPtc zQJquRzF=V5Zy5=hwlfNf?JmC#A(O`=$6n~bPxJ8k` z)HB3Q^xy0nmJek3erqo@Q}@>SXWoJGQ7b-ZC7%?kMR>QpQ_E#gw5_+i{Acj{dDCFt zj8t9-0&A%1d>_e-B-s4bdh6UsLsh~oHli)RSC1s7e3!l2WJN({9*cyeZzP|$4)tGk zqCcYU4fR^)$Wot!`bL?a8}o^t5|oZJQ0!BDO~&-Mj=;BybH31qExbYR9<1K2^I%Sn zCZxp4ZOA#=7p5X(|1|~FuOT;&p?(!P$YoNp7_A|>7(z&3$J`p?Z8FGhvqBB*cJHcd zuey-Fp&Rv0b_ZLb?)R$;{#i5Cv)`!SYQIOxXkZz7SUvx?dWLiae>FI%SyB~E%7u)+P%1QC9HcjQ%95DEbe@_Pn_J$TIr2|%_DsZC7LljFvip4b^>En&8(TE z6N@|3P_GV9mD7C#D%7@j8KPo~J7pS9$ym~hj9N9bUY5KfpY)0qP;SwFVX0P_RIRjG8%Vo(gswnbaFD1c>bN^9qg2Gk>-7Z0EHvr0d#unwuiaXPLJ(^eFe;nVhqLOO7 zfpAVzjm9Y>nN_>0w*9ZD!$(_x21Vc7yG2zQd=#Z|lcAB14(mLPc9)-8#@k_; zmo+jkpQL$-t<^`WAso|$WPN{9*AjJIsv3Pi#IBdC)ctsMO;z_-C@e0AkAj8FZ-%i` z{obH{BP-ly^>?2nh+3}xHmPgALI+Hoa5VtUtP2@*y!|`s!ia2Df0234QosLGUFV5! z!}As|pGYHFZ?Y`{`DR}?IT0bx<%Q6$PazBW%yvsy7=yx5pMYam3YF&Q3>oo z&?}^c@o{cMwItnB{(>|y!B2PX=x*O=iHxl15>>lA3VKrAhdcI746RGqVCmmO)yY(J zJ2^+9Y7^aGoz?1OOAHTj=K@rn0pNMgt-+K^&BvZReI)I{@meBOWkgz6quQZV?MS?a zJ_!{eQy};GDtwlwYb{Mit@4jMC0StHW*T}1{5J>~Ruf@&!Z+Zw;J{HuxZ-oYfmm?{ zloyvK;9vu)9!6Df!K*7dY?)`{4|!tagbtZD>rp2b@}@1%l+~DyMB|^UGB|KNf@w67 z3K!y?XNdDMqzQVQ#ABGLgoTdl4fgdx6!A0;(bU&;Foye&S$GxdOrra)9t`e`ae8oX z4~YYDoHGS}+3QPPd4A3H~STaXBt|!=^w|$dd*dj6oIyd2!M!K)-2@>c;?T!(5`k-;&LMY-@gW2c@ z>Txk~OJo7P9QApfdI|OCU=Wx`@4`LwU>#*=hSr7icZ^rA>-P(~Wa*(Sl5yvINJ7{k z%R*{D_;1_%KCx%Toik8;oh%|t_XqmkcB)Q^7I)_Cmw8a#RFkc{7@}`MdVU^RI9GBW zG_Ksr%+HmDhuB7X!QgNd=8v&pP&f)&Ll%w%91DjJ$D)xK7ik1O9Z#5iUbDTjcrlm- z?6Pep#~{pAZz93`WRo);zN0SqfIY1)$QN+z0}Swcg>Z`80mvDk`e1KeS$mB2E~9 zbh-9MhByGl%zRLYTBBP1Os!WfU9eb#-8S6_N8JkOiI56KVUIVdz7u z*P_}-$Y6MrVk;32``@`L~TA$StVa%C`9;_km9&@VteBAmf zp^vM4^g;T3bxb!o{eC)IMfSrd97DV$L~8bXYotH?Mv5xmrZBRcdD$^$*K9EK4S+~H#a zM5|F}UcVk>CfNBP*d3a-qYiRwJG%{8q@zUZf^no~lpF`J(c5SKKL7v#|Npdo37i$h zwRd&*y?uA!GQ%Ah5Jn8+b?OeXXl#}sXh1|%61PDGMWazf+svR*hCzLzCN8)~MB={2 z#64V^3 zb?UTT=$e@lu_i|dk@o&$`;HyO?~NC3X+={^mApYE4Gr6^`FJ2pho#S=Fa!U+<9F=X zVRud=7!Cf!M&UMP)q9)hJ@0?ECsheU8g%JJ8SfN=u1^EvVU@kP~ zBE-A_kBE|Z?JQf^#WM(23zY)S>}ojr*`2|&8;TbZmUhttsJbbZNa*`^@nO2rqF~R| zWwjwy*Qj9^A14T_+X+)baxB3)h)N%fiM)Bv_8+1rdZwgZ|CKlqXTk15Om324JtM7K@d3U}+R8FTK8M^oq9iyrAIXWLk z=M{96=%B6=qv*Jij*IA6r5f=Fp;-Li2H~cgK$|*OKIyj33Q#Osj3Uk&#U{beT_sOT z_{WYW7IIaAx0cvW?!_S8j`QeFrYX(0R)8am56}&NE&VGEQ*1{!7UG>JJ1(M!vx{rAL$gEc)L*xedIjYFdYX`O)bA z2NV|oLL%v3h?9Q|-Dpl~uxrr_i^2YoxR+~h-McByWw(K&a~kd%`X?{rHw*qF7(@;~ zwg!(0HB9N(D*Dy^8C~a6{)+^zcLe!<1eZu_yZ@CSG)HUezOSm}2Gkf+WUPzlX{y?OYC(rDbjw4{sovmPVZRyYy`cnUb{z%_o7phu*PwF z>g$D*r=NIi*ImWQc{LtvQPAzRo#GZCAL}|0mBzakRr5n%C>2(2cODqwhk?Ra6oE0! z;99&sCep+!2lW@n5VGM}4MO$fzV!TKG*>Grn5Ax-m{bz=NIHsiKtxTW<2@ofx^A(; z@>p z>#9Su-WsaQyO}QA|7lxicKn7aTNAYLaNS0nO^Zf%yhqYKErsh8e+rdmrUpt}msD?o zD{3dZy&-6_Dbu{@n{$HqGpX6~m;delRJjB8!X)WoF z5yY8m+COeH*tIC0v&BzEYMW8lZWX6xtl|o#6FCCY?Bk$wTq8Cav+W;CWND33o!hxy8d78Ga#~;I zeo|AHJf!`UrKVj>gpNElH|;Vw#~(?*21q5L^~DFo*nblNZ7jm-Nkf|)Xp>U30k2^` zJX_OgpMW;=6v8?Ac4Q)7(SA1J?I>lR>0tX=2YO>nJ$t{~`W4mwoV-=Osfe4C-wBA@ zp!upH&$f%B37=$0mSlu1JtJLzGnKv9 zKIiB%`Y74;s2KaI7{5Zvu1CeJk43`J>hFe+41;Sv0$skT>2<26*RdEIt;c9~=!K6o z|14==b#yuP^ccC7WSS#lWn9EOe6f64^Ub-MixhWxr-4|DBSbQEE9WK!*755>tUWSJ ztloI2f<}smzAtv$#x#P>0XbGim*(VrsO#?#GTZ5V1RcNAblD)^j)m=McqyB!Cc;#o-3m+)AlCu zg=a$AE;8fO)#I}{rD*$4I#SS21#90=&@o%nJfmnkRpFGA+q_F5&MOCKFrdCz-maXy z*_rtONXWzdD({(c`UPW6)34Bn5!Zb&WWOGCA#;`q32!$--EFV>OkMr}yuD68Mpz&7Y?E>bDLJ!IvuzG+e3YST*88lAjd=$9E$BP5n!bvUFBM;S!0>Tg ze2a+il=4f&^0Ck_Q9e7?#faE@(tUUacBPVS5NC7^?@6OzN7gwCwou-q=@w>+gW3Fx z$rrsBLAe&ntEXKOjHnt^W@?sCY7Vl!b_vtuOY9RN&E>lm#C%osILIvVMeO)?&_xazQBF@G#ZOjl0zS1c9wS1KxQ z!hUMM?wA_f5_D)t`u5ndLEj2S>mll)_{LYo|+F@Vp^U`#|lmPk=FK6t)#*xF30~g4?X#EM2%*`PSI2T!vRTWXp+u|jD|0b zv>9Bog$K2~;5GbIE>1N z1C^qEkJGK6(E-u+`-m6BQC%_P3QLPjdg&E!HGTyHZaRU0SbLj}m+4igG0k+h&SK}z zVRU?f4ye&5(D9!TNXn>~@houBgYQQk=J@)Y9ue}s?)~6QYB?73&T8=Z^}87VTuff( zy&f4)usYn*7Q>^^V!Urbq9zpK(mlU4?~!~6y5S=|EqoMC3)B?c*+t6=$d5#6oN4Qe zQY6^s;Nc-3BcsC%mA}!zSa(;3Y~q>hnT&bAG4jpLX{q78MnyfVoCam}WVER9p2CBN z(u34|?i%kNJh*>0xu$({Fe)R=8j*au{^@Y6&pZh}bw=0Q6+`veRLi`*gmOOpjSgT2 z-aA@^e{kNOOl40%8VdG3fp>19-_KO@PC!Y+JEzgDrIzL!5h`5H%)PCU*WU2PS)p(Z z<@6gMI~~O5;t@@AXm9K2cwR6^LVi0+RVrmnXFMy7OslyXF!(E5L7knMqNzb5+ia!oQTC=Y=S+UA6(|r>F%MduvCn?hOZV)x_pqrMmaW+x4VMCF^weih0^+0-I|X`nIee9Wk7KKf9BD zoIs^$Drkhk`##~ihaLe3gYsmv>2CXlJiEPqnb8D`R}t0fTTBl>>G)e|RJSe<1fTAz zTjW+V%CECqUkiHcCarAZ9JaV@DIn$@hnox9*DVXQzMvOSo4<>iDsw%Pv+Xz2Gq&o~ zUi8TJPXj0^qc6WCM!zq1Gd9{mSlm#wis$12t%3%rmc|wD#})4n$3c(%uVDVSXTBHU z9+$uNV6OxZpQ#B9RiurMsDOVRa*F#TF1NX@a|0^9s>Rf+5$)CD=kAeWWQ?bcd@O6Q zyISdUJP%|wD$365Y0tEeSWYzR%|=;(n_<_EbQpH3B|`oPYcjUp{{?HR&X}3sVZ=|| z`!)FU`$`kY}_%w!D%jV}>U? z4}cQ4+%Vuh@(s;da)^!$1J&u%YMxZJ`QtTD?vYjW?#--e zZ#pi*j!}LbzjJ;x{U7q7F!I-8J;k2+A2s;)jdFf$)4aH!=FKQSzNz@p>LWkeHThB0 z;zy?+{PAE_l~bOiIGX45xz#*di8p08T>G|QH@+?`4>+xT>RDp;u* zq3!3lHGjUX{96a;wIb4!9;AzE)(i;wD0iDWYeGgq|lSp-OMtro)JRVlcdl><5E_GOK%z#;cUFaWH2M&n)$r#n zqhtT_VdWHmp~jU)=vB9MUce5o=yCN*WL#-f?xt~Nc>i)9Mq2n?-m@@b7i5i~YRBlB z{5hj*4x!Erw}}Munq=sodsJLAKeX#c>0aP9NZaW;1WPWlu7mr%UQ)fDvAD(>Tt8tr zU5op=u2Q*P6Rt`2y)C`NIAkc=a!9yN_Yra53)Qh0b$MQm1KYz;c{W}Po%)Pf`+m(X zet?zit9T__aOqRt*?8|T0#-*Q69s!leoZeGh_}{SymoyImF=M8Qo06rK1SEa(RmI% z0}Jd!I$G)ar&P9>j#-xJRex7VC8mmh%NPU?n7S{^#~YT;S5`4S-Wny-22G|7A*s#U z`&TjM&QtIKv*3SsOc+?PxAH=v-1X;T(UTMh%k{sR(3k!^v)19?O0UgCdrSby-yx@l z2Y?$GaE@;(SL?k%V;C2_jDmxLryKcco+!Krw)YPW2WX7sciRE@UDTB4f)zn@R=z32 zMTg*-BZ9V$=E}8Dp2`s-I?`pgexicjS20@j?(aV%NV8pk8$ye8M9iC1)_uEW40hLh zo(l5u@w4EhHX%0;P??%&^}Bi|xB>6N#MH&Zx%hk(9}19JF7Q#1q=M$|Pi*}Ei2i5W ze{_O%W-?-|Z6d+l{i+>d-`(igsQKylbi7K}f3-Lc>3eisPS^jJ&QL^tV)3$Qo&e0I z>kq*GbbSO=Qk2uoGiiI`phn?#9B-4fTtWZ*LmR^wluHw($p+<4sD$Ux9J-hu@UK9? z%*j;LY)X2ttbPXlIt0H?Bv6K*=P5HYA9FiS!mWc*f#%3&u==vV%i?;Y;6#`KFN)zb z2XCL4AixXpqUNFh0~WZ(Fa*5W?#$7jQvYa*zXJZ5Y8hMD^_~Wouza|I{>?Z&P?%ir zRsdUQ7vD!1*VZ`Hh_kM+g^j6Mzw-{*PY;Z5grIJOsM&1zxD9Yt1@GKoz^%TPfT?zC zV!8g;(H70X%6)N+zSZD=JA6(9x(EL*!5eyXd;;zR8bMt^#P#1ViiLzZoFU0a5hOxQq&Vjxd~H2h#YD^o(&E!DXbiw||GrXi;MzsN`!> zyw|ay5zZe?=GK2u6&>^sbH}{3Eaf40H_`)trUU#k*5cX2l>c3qoA@D9zQbnyqh6Qa z;jlC0F^6r{dL2`N=RIowwRFr8Cf=x*xi6#75gWZSmgXy;H-G+rf>9Y;^wlANaaf<{ z@m}LMovy?Bs+05zqx0~gz~k`2<8j1L;BiF%@u=*VBO`cxS8xoIjw5QWir6h_m^d4q zt>7yC94r98!V7wJ$SEDtBovtUWubk-@$d}pL4ulBJC`V1$Wiy7abCODL=XE#5@D_N zI!5ZQNHoLK)LetBGbPU9E}%k3iJ*2Zf%ZIBF^Jt1qt{g*1w2^UUy^~fIzaLjB959|#=N2cTr6GwSNGyF0m<7-J`%ydV5+q@jU`~;k zJb-G31f3vwZ601SvPuLQB?$Xc>(4MyzhrUj173WB`Uf`MC@T9IoxywZi#$iB&l83WUPsuA&r3}w{DS4qhou})RcNW3SC;VLq!I@VJGY_WJGK%-_ z0<(X^(mIZhr{V=*kpp=MmFo3puWP!>J0x9KU!c zX34u6av@&OILxp5+j%xkd23Mtywa>3q`cGMfj*GtuaxS{sk(EZNaPb|jl_JA)Y!>) z^U2aKx{dNxDe}q4GDu23jo7PX-Yt?EmVPf-Z)m3j3M0a(4>UYV2iCkt+bUL8By~B~ z8^D@dt8D+;>#61Pg+2L(Q*+FyN!#)N8-u_s6NGCHu*%{5S})jJ_I1sbh*Cnt(l+gB zWPq+u$fa9X8f*M1IV$0QHU?4UdPU)0oAyU!ap!@)|^XB?&(Mvz;gYYyFDxfD}A9MXX;Z;K`3!)cZZBZl03 zl(4u1>zeze=3{hV1sp;9&;hW@f)(sv)A1KNwqm@|aXB61>40L6H5|MLt=;jdSJPR= zi8=XTa`dc>v8rlX?hMhKgoJhl!{d8w8hge2a>)Nls{(Jd49u)Sejm2v_0y-dJdA zDtN}*iaX2Qj+2%4mQiJoqihlD9cd+Ga^CG`sIf|;7r4F4P@IG_P0>wB>9mpwInT$v zKVfr#<8Pw!gp$SnySTLdZk$+_r-^zmqyHucBwMIShO;IR4{0pEZlODSsu6Mvj0`nQ z7$5nQFt$sD#jm%5&bh<8&eHshm?6z82*NS`w#P9_1RGvYQ=1^8u`Z4cB7YVv-)ZI) z6RG^Zijh>x>E-f!J%q&Qu%Vk+@zMdFIeD36;hd8f@Ey58)AtOjZ@lyzCFz*Myra>p z2s?5jlym$~$7ri9r-Zi$R_^#a6=pO-3BMI48TY~{r4VSgL{IVHOe!sAQk<)Gg{(AzFv`^)Q?7PQ8Q<0H24X{v!p0D+cD^0< z6x^suHKDg=j0FC%>_~>wXgUbskp*4pTsvIEze+w~{L2@O6yb!+f_us^BW+Jog1hVq zOag4-|G2s5DI?)$!}&U&W>yX+0KV{XLH$Xpi8W^s>Q0fRsL^;`ic}cTtQNS%d)+re zCK$c|k^O%lmb_kRfBIE#zT(K1d5gJc(Z!?b3NX zv|Z?GCRqQ5o;dvIThqr*w-N3K{P! zxP5D|xTu<~xZY*rO?y%?c*ZNLVQSj?sL(bW8&kHi$FTf}7)XekKPZ{02(Ca)PHPMA zE$MdL5{vF87_J*IJZykMB`xoDT1S?}Af)B>^0~?e*_|MxbGywK4Q*WGGN$e%g+xPJ z7xxzMN09GrBu5#))-Og(p?}wFH#qmYJB0B!m7^;+rK7*xEX2kUNC4y)4+xkL?NxdIL`Fg2kbwV8i-!AydfOMM3h+#Hq*! zQ>DX=RJ}u-`wC~nH#$1&CsdRw4Kwju58}lix`uy`qw_ZOzamkZ46)X+GU)C0eNHTI zQ4JCIlxCBs!X>_r-LPouCJ37U1-jhCyoT}cx~j`kb(MxwO;?cRWMz{D_x5DCiu{HS zqIsh{h^BHw@y9W&4f~(Oc;7SPly|3&e6u>XAc%1}luBU^UPZ?k!TS;))N8&g>Db5O zxIxsg$>Q~+ODqoX!*a?$1ltdAz@G#p+Rq{q`IpliH)EebRfx-t-dMU)eK?#;E|y1e zm&BFt)lFmYqbzH4o zCkd`khFf~y{}N{HBkm>e#$k5xH8{Mx<5-NS&#A4pg=|3dH+4t88Ck2%^7}7=X*L!z zZ0*OrC!o54=@SiY8MfNvY$D7Ltns*a^>)=vh0d6%*Yl8z^QK>eOiw3a(sQ=maK%ZO zp=sb3CQSZrKIWgVwE8%|asVK0IJI#W49!3aX82|c>VCweJ)&|BE>!BA5p5rUmu4_Fr(0P2>=lMlHABNMl%++#!)AG;p_4a|b#|;G@$Ax&*%)`(}Je1X};mjH!E@}j8 zga*?;$Je4ul_j^+ycJH@@q_Q56+@tlpAdlohs-ZjynRCHgs@2v(}R9nz*wK4yySJ5 z4uhIr)N=U6YQ2k9Q2CHMZZg}f8F{CyiztB&U8)f+S~I@Hosgf*dQXPa0{qq<-4$(T zu=ltKKRdmDGBBEu?mB7kdyh{Xg1yIIi@?}-r!Xou##O)wBLn)9!{l>d?H`Vvfwt?U ze#g~R!ElD6k4_#6JWd`w9$)V>9=-OB)AeQ#1eJe9Ye8DM{i#{Q5H zdu@^;%ZmpOq{D;7e^+_tPJv0}Oxm9K)tG3w#_jsfCKYj^>|!;_y+#b|Weh@d=*@{!40SG zi-n{np>Eu;T^q%N;3Lkjq+wv4C9)P8q-(%x1`NJGmFd9YT!I#qCTIis=Tet&gpN>! zPee@I8k;CJ|NLCnZ|a5YK3;GPSMYkLZdYe@y9UHTjP(Z1L9ExO+4>X}oukO25?9G8 znOnsJ1=am6pD#mqF3VN)n+oh<89GwGWfw=GO&$2(b?TsHL9r0gV|Om6d&d`O_=S+4 z`V#+GL#7lP`ix8ObB4l21A4(`cpDT>6C*f9;LRNvyy9*F zI3JgT;aSB;-Uv*#;O6}QI(@<)!^-~RYPijtLFP+j`BA+U&O^d7UrV$}yH6gx&<#Ak-9W zNkx-?gNb?~uivdkylL9yP+FA+PA`saiD)LHHPWL)q|M6UX1xGSK(fCw_;9dqSn?tP zU7jCVql51cODynknKe57m|COLZ`WL->q#Uk!!HcYOm3lF#T5^zWgWU6ExP|IbUa3s zzt#siBh4CF|6=T3yE2;BGMYQApRsy&=jo{+AK6t{20PvOsQM1GLKYa9BPwCS*A12M zS23->W}v?&6zIt!OgY8?N=IsAy+;bC<2bki_=fHJ$C?1`jC{7Xf1tM(OFcggG`6vL zq|q;So&&VMeo>VJbVHZ}6j4eKsx^V7jX^fWqp7B^Rg{jBC;kgTf{$$ylgG73lIx(v_<%K(7Wlbe?dQuzT!Zv$h(CJ zxy^3Fg!E>&>td8QsPasrKt+kJOO(ccjAiu6m|FNBEG(Llx|GU~|D|zq1Ya3DL8fhg zHH-;c{?7JSg2eu{jk+z1hNS-DDGKUJ!!I#`ECo^92{eO1JUGgSp6*6(=X|JIr-ndU zKCIg^Q5%J~!77TjRvS>?hG?#6>*flaAefO-W@QwoBQ#D&d=5_SXujjal5_l#>VIg- zMG(GDBRokXyuLQVoaaomKyO}BIbOjm8GWwOOYBadQp(GSmV=TO+rJdEOKVccWGUG5 z`#5GR@DK9+iGv-5WwAE3`KkAihj4ZqMt)JeQ&RRcc;GjO1MFL zv9?;v#{-=Qip!rOi+y%y2jUYZGzs zN3}iDPZ*u6YuF?G#$)wP@#stcbZ!^`9#wYHhQZV0%AvsH$`FqM=0NZKrkdR@Ag(>G zWiahIXDHfp&fxp^+@ZkZ-2V4(rTu+YfJbCL4r6}T>vz|O@3K5NN^n3F;>HM*v%(R- z#qL~#3Gi?&LLQX}C{z8XZg8DVe;_DFN3Z?KG%+?ep^9`xbWdRFik7Ht-rv4Q?e)BL zP3?8X9+sAM>XQT{>ss9T&|o2>u%3^-YuMQN79oSQJcwC`s(P-#UY>&)Rr#`9VPqXw zG2<pWx^MAwZ5d51k&Dq zc&@pO&?bU&jadjud*kqwXPzQM`@%-E>!-JF;XOcW)C#8N*VUr-9041O>;i2^oed_~ zRTBwmn;W>+;&o=1Vp_fwnH{!j6Kd`umW4~=J!8Z>7I2#y*R9Uk(T z2rAEORGzPi%3x$==TOXror9l))(!<8YX>j0yCQfDN-x|y9{3q+!0EgG_FM>OF#hr> zeSEik?x=6+rQSZ^5C3LdAPHv7iugD0)KA`7U7KB8Pc4>07LqD%17mwP!Fs6|P{fJF z3w{?i6Y4a(Z@8_z@R)_H(PRrU24v+UROqfO{vKQGGEG92-Q2T#bL7cFrl+Cecfu}S z)L7=kNGRW`@?(_5$=8SxypNi$Zx0Is z_m=2-QNPE=I>;FY?H?qDfT!Z(T2XtBz=AuruPd1sj)v0y6>-$Bo8D@o*+4X%4~8go z?1_~BQ?P~_=}DnpxHTdY@6{sl-rCX`KCa7p!4H1NQ8n2AxFID2KL~X0VQ#-wuuPDRN#(RhcKrupEkJR_17f&o5;lvVcBWyR(W=8!B z)zTJ4P$vo8(#C+U$E^qTw!#1qe%*xf&F$NRQJhx354u7*Y3PXsiN!Y z|I)T}h-{Nk&s-@v2Y@Rcws=fveh0!SQ zhW$+dP`m*E^Z*S~u7f?1W*ufR;`3n!A&e7So$(ogxI6U--KbZkLHh+JJR|~VaDP-b zx>SeT7I&AbdfJRma%0ajooM~aVW1RK_d@xao6+gf@d<$*ui4G?Ob<=OC{5hr(`*l3 zXl(VYcRa%Mr(n;3SsNDOeX;SIR(!0*dWu;i?C-dX#T$yZ1$#>3mwFlguaR`aiL30gexTwbjuZ z4Q)gom%$)kl=t63R6~pDwWUSv>*sa* zjOSvHe__t0LH6W~_h)QvK>`K0h4s7t7KNmhWc`k=M_9>)TKDmCTm+HHPb$ zHRjzujgwde6yUr{Pxe&p-I9LQ=tW{~OP@6~dI1}zurCm)qOdLV7j%Gl2pozBH2dG{dWo8JBY33F90B!U3v?XTrXELgU3~R%*4vrzITJI^gg2#l#i<+YxXH2 zou53}Y;^iE{fZ@4E{(68jj&O@5zK4FzyN~up59-jB6yb+vhkHVL_=@c3J4k-wQ}mg zW}_AOTW&IY#(I#Yly2wEW-pZwO~vLqT!4IN$YI404$aA#ZERmtpG|P+eb?_Hr6++oMDMa%2qf@H2)!meSbqEd z0QjwHo``?~oTF>=+jRPYM#H7{tl%8~;32pH;12vg*v(>IJ5KYM;u)rIBvRvOjm^;^ zHq=woPY>)*?jX5AHW4pqmb*YsVJd4LLuLrM!tviG3OhdOUg@PHY{MmL*YYl=Lm=Ri zrD28}@m)~a$y}vba@T+*_Z(&z8(%h&mxsg*IKxq#;lD>!Hft{V0k}lP}m z#yr#a;5W6$RrkHu$Ns944{^2-hR)823+#P-Kl064MMx-Dlanfo&TBh3KNChd>coeB zE)=X_!7fBXMhse~Fl9H@Ls_yyzpVA8r>S%z>qzn;52*?NXH?%^x!FCId zdSJ>@IlZMu%Wl$E*G-Wfnf#q4d{?2xk-|S+eR3hG3Nd>mTI=K@>@E-O~)-ldpwHgP(?G|7kW7-Cts?uCUu0u zBq#4r*u}FOkv3~uv;MNm#+`)@%6X1{BXRX+wPzQlIo>~Te<7{Y1VNg_4CFnJ)G30n zdtFX9#c8`xJXZxu22Zxd&DBy?d$Vq`E$y~o>wUqbDEcde!k6IbD$~~o9-q0)Suiak9)RsJp9^ZOrr+6&APgMnEU&w4=xo)^@B*)M(zV2-GSiN5JA{s-UgL5fRW z+eO1l2L<)j_!^ED&(DLq@O0#Jy;W1ocO%oq&Tv2;ouvT|K)Q5TfWsJ#!1JXA{pIjIt{0jeCr>po^S6}w#|m?h7|4XCUnn1M2tRXb z5}K@xx(jaBD$>oNiWJg>rXX1u7IPI9%$JW;@|F9G>l#h!Fb+U2{@7NGCBYR-CSO+@bT5DSt2ek!XS6W*=(K zZ>qn;p(;nZ^25le)S0L!09@8Gi`tHVBoS=&?uZ2JBnsBWy3ww>_`19Cov{&8 zv~N4nIx`QI6ynrcN>$&X=gHb?m1xIl5<~Y?mDsZ)OX*GAn}~tiyu{ zpD}r|ZJ1A7#lBZWa?bzfXIfjz$IRN*4zbHPYdmsVV;&yhyiwH!a}vCX2C@M0N575k z0qO;2Wn5P;<73|hQlRMZVV92&H>t-5^eNn=JDu-OJW3w&?$GCKC|?}=-RztaI5 z^CMyGB=qadH_?bXRn-mNR;x_tciySGH|*rHQK>vty8Gat^7b$;>YQ4ONd#o(cH$!( z-cxucFHA00EeeUubMX-dO`{B+M2&-IUPyNyVYb!s;0YRy6!+x zLUX+1`oAYJ?6xF~*wQ>WTltr#KB!r+70}0*&nlYpMglSTE0u>cr-Yg&ks3R5Pr*GN z2fzC)GI`h9$}=%CG8m(b=FIyc6Tb?q8VE^FRf>;rnDOITJ9I)S&V1U5=O@BDs!5`` z{>0A$>ONwe=Q;0SJpK4*q3=9q6-!o%edlz&x0T}92v>Zfd3`7Tl9zNE@t49h;*Nh5 zr0zyF?+Cd(H6K1D)P-iI$fJ{{~ zG}P?=(J*}|>HckXSPM=TP5}D%>j|$`+p=}tIv#HOo7(Qi*SYP#vL$!x303XyX@9Bh z8D+E1@9O7VbJX&KYjupUyR|g;vZs238yJ=eWr)B+0QI1TL#q6*@cdzhTe?4?%C+aA z+k1+`$5zt+{Rf^*^e9gR;+7DCq9T(M!LwBJdFu=vO>r(W_uT8g)0{<20AXnvY%IB+0bdVM;*Fm zBg7`6M~AzV10TS^5DS<82v5UE`OmmS%YT*2zi`P$%75o;wEU0BZg{K%=Ehv;;mH!^ zd6taSseB3HS7`js*0%dwO5^G8!z_=+pGz$t00-6S;(1GqinhvK2ZSWaDD2NYA& z02EYB|Fa%6Zzwcx_#7A;o^_kX${*r|%75bj|B0sBIo$9Y%i|b=yHbA_@8h}h1P(D1 zp!kBhC070$k9@ToYu@0@7}13BO7|>oMP*6zfWi=8#PeO_u)@43_bAM|OKlCzi}tf} zSB@Qzm~24ufA?=#Ev9cc(B*3zm`=yLu2RFqwHW10F-(N!1Ek}uSy#F1Lx0~S>qFdO zC^aY4Ano+`exU}hePJkeg2wMcna(+&rpCFhBC0Q=z9EdkH(C-H-x;7I0X%UN5I-w8 z-f1KolKFu-!`06CGSqIK<9^!QV9wGx@DsXSugiaZfB_e*J(+hA#w+iRnj5nu7+;jQ-^z#Pix@(ko!D>A&sd_Nt_ z+I3ee7hT zS}5ND7kZq*pSLY8`S9!%~K^pRPsv&3EYYrI1EL z)88M^b#Ge|xh<#b*?NpUMSmlY?wuh~6M(J0&6W~lNX-D=u#oD1#j=*B+o8tIaFr4V zrF-lIGhoWA?aN!#7bCmNdJ-G|EIpykj!Xb)e64xB{UtxL?9ZAQm2eIQzxJBHxhz8&8~8h&Qvi>@Z4dsxfFw(r2-QIj#wIezWZ*J%7%nf6w`kpgU& zx3}<@O^~eSAa%F0%pZW`{j)Dztmh8zfhC^84KJbte0Y>8f7}z>x*AnXxffl!S+$7i z_EQ<(s2c&-VD^-Za}KKH%7Zx${X*q$yEfaJEI6YooI$n z<7SpWxcDOL{9_~i>hR$}65}Gb4^exp^O5#8{+ufNc;F zY~z36{<#V6pFv;kEzQ3aDeZ$)J?q3TWbEVA*kWazH(mddhb(sH$(hM+qF*s(%Py;J14OPep5shW}z zMZM^5VOB59T3-d8h{F+pAy} z9_cdEs?c9p0dN@rzY;JnB5O#YSZM?IY`ir**yiKoE=f*6E-xKqJUPWKtp~@Jy-RrT zQXh^-t*SWTsWIkVVRtJlKH@p=(B#UxqRvt0LEaO(^a)*+fCttwW$rg~>iE-Gd~dm# zDPg|mYwCs5E({#kv@*}hArj0#xjN{716%Q@tLL)9UaD_hIAicKPi@h;lc_Unc7@{h zjKRw>P59yE<6pPS0_(Q5dF>R8(QWP@J%+vPie#nD{fgwMHuo{&lRIYPX2;x8IIpaN z^U4Vuv$1+i&~;dmgMMZ3Aw*8tslip8#>*J)V@y(sVs+5&ju-FTx8349KQP)7u*7>H z->yXO_5jfnv_F3&Sns78F2Gb!_z0fM1s+^f>{%zT*?CkgKdulnTvu)ZTviQSwl!I& z+io7bCh1q+l=FT0N9-35gX&1aitEcgT=)H|>}7aVbN3umD=Ms0xYyN_!DP6rmUPGn zW6N7vZQKvg@oU`otDoOVIt<^hKZezvBvpVj!|Hq9)6ai`D7tAo9Lcz zf+TpTH|k6sb!P3T?Uq&RlT|1Tyf}p&L~?nd|d^`3h~<#P}5p1Yksce{Ge)u;U& zPpfkt$@6bDTbQe%a<(N`gX{CZg|wV;*{%1oqxSY{*})yjjts&d$&L=fX~xb*23I6C zm)GX~T1PwEn#SO3I{LY|!o&H|@fLFYEDvv20&k73=}=vqT!QZunpx9h*<%U|) zxu2e4_ zbEgEKt54%`OUEJ!bT;5rM#rb#I>vT+U)Y8IRPEC@2K`v4ZPJr0fjwpt;ep#@NO?W@ za6k^Ifw@lkO?mIqae%d8>X3ZE%v~0FFvCdy=YTD1%yKWrf#>rc2^7v{UuZm-hv`Zo zy|M$J@bsjREJU1M=zCkPOQ_z9-`@&dR1N?2{qe)`$V9;L$oS!SD1ak1x`C9gT*_a$ za93MB%;?|cl#p8L2(E{GxUxE?M^}G1e%|$mfw|kp9Y0*^q8&;U`?2I^Yn+XNK0Fo} zKT$b;NBw868sjrZxAU#?o>`moALH(^b?@S3-XE3rk7W#wrthJ?QX6#hu#7JIM@n-N zwY3h~qvLIB)qJ-{*`xVGS9u@Vr5W@_M-)>-iRt0O5rx!HMIeG}N1r*?e_~o!QEr;_Myf2mQxwu$B&Vp|p1Y@`FO|pJ%`2x~{MRJmpxBD7$u&1hAZ5#GCJu z&UeY5Z}S8jYg5ki)G=I`eT!ylT?LE&juM(%)Y<0x05r~DIc}+#EBg5n2Ivbc{HM{o zN9of+ch05j#q^m+pB403M{V#NPSWQm^!*cbMGD3T>G~9WFR1MeOZ+&A+*yCI#8in8 zX=JFmEF@UhMfW)84A=2yUxIv*GuJw;bSoFhMW+?}^3hy;1>5E6COiSTdbi!d&frbb zyg!;FA^awv%tiC5Vm_Tq<OfE@ZGHX}R-E6KQzfCdUnA;}T*q6`c;*(Em z+vfPv2{bQj^OJH}YM;a%>0Z96m~YNC<(e4=zgr=fr`E#S6%v$H;~O3M$} zb;AzlVtv=-X9ZHjBudBANOO?KN`iul&R4Kb$qFpW(TI7m#!-ipFyyDC za&*x-65atlCxz65f?D>$0PnE}eP07R5>VsLXnffUOlF%|!A>W1GlvnEvy%dS3j?rN zq<4QW*s3U=%iD+4b(|W8s3F<|#~jHfQgmM0Uw)XBW4`*Ld ziT;%;BMf!E1Y@8Up=41hI-kR@d5(IpXV{5{nomH#2>L~(;ZV4AL}q`*Q*up`_C3(? zfDt!&PSKeT2b8WoBrLD!6mkFIVYixf(TQ!ss!0q8_#2Ca|j_K39 z8HIebK#P)+E-$0Ys$mJ`;UXsHeUGp2Q(vHJ2JRw^O~7s z7h>gI5TJf&D?5??*7f!1u-kgevuqgp#cqFZtT4NzM*kJ&Id8rPbiN1t`NqM^N5euI zv~-x#f-w8KGTjX;-JKj`UjsOB9i=cM7xElF8_o{6e~1ESjrWpM?Ec0xt)>0v5O-hx zfF9MT1kyZU<_G~bciL_3{04>Du2$5~nRvEBeRd$^^EarP;i z)HI*y@j~1^(YYQB4gEK!w@tYwpWYr$UqBfSrWWM1`Z|5)(%;9?=lAq^A|(EF*ne;s zdnF`sm?&(DGM31Bgk$ickod{{N9n2T;3jjsvkQ9UW66BHm`6sdL|;BhL@||%A)(dU z6$v7h$$UeR$Yet<(?=9C8l;Ka4f-|aviV$*sAMiTsV|d@`knH*MrzCxMPzE$ z%)+xAAO4M){@5xB-6C>)yL~STp@BGoDT}{q~@TTkP7pl<2;h_Pl z(Ig&Vk=gx1C7L8W6fw++B+*iuffWHvY?MBo!E-vQ*c=V`h7W_F#9V^XP{<(GG$uc0 zdOK%Cx^lo%4W4`4 zZ4SOEt=Cb-0FR$MGQdwpeE!sBzB_lVBbGPorfCjr8sR6yrK19Ty6ySm+MSN~>yIgX zAG7MH8Utl3-FlekBx=1PWeb? z;+T&VI_dF|!eD+q!2eBCPV!>rBt1VV^*Z3JPxFbR6bAEnpQA){7-L_GgGGpsWLDVY zBZb*yKJv+r<|Bmz1Rv>5=^l!ad-xb>cV(30d>tF^39S*}@sYw-G9Niz_{eCf&*LMX zrg!)|)3sDC<YSZ{?SB#tN~F9oI;s08y0G8m$i)Hmq5F(jsh zr|9!*&^`Lhq2F8;5?TrR_c(o)GQEpP0V`r6c`K*PMP^AZHu=O{{DignNTP)pBBl!| z=M)g3`5xbu;wax-vQrxSz zF{DdKP5;ECA*GmX+7!D4=aOxvyHDsv%6S!YexB^%l=Cd^#|yofHX)EH2XTy&_)ilv z5)F@)-bF^jgwpwxgZOf14n4X^l(HagUkH+-g}M_L`a9A27jqQVE>`Ge+MbuTV}rq^gTtJ;6ui(dP1gToe9q)D9zHua(SP(ER!c;% zy+ofE=ymZt5Mjh*yX^<~T&9INREiwlG5d zE~o2b)b=-O`-LqMFb;C0GepMs1E|kw$}&Wv1E)fdP*8Cw!@?*dIi1lBC+1>PPdtGH z9r`bE!kTpc$0B{ zO*ab8nRsA|HeLFJwV8Me9sF%_8;j_mCQMsLsY*3OH1Q>~OMxurxw6a=uLP(6)_86TIBn zr5FiX3(|JjCXK2UI+JmF@w{}Qx5;COGLAu~12r&JH%-b;cpE4!B#7Q0KTZ{T-xzjt zMrNEpZsZwC%C~nm>L_jJbU!S_mfj!I(V0BHH)hmMZ0m2<#AI)?+KE>8?*vB)&;XZh z1aU6(9*^g?O53>@^hk(lEz)qxMxmKlTpEw~f3AiEzpg-dBwE+Pz`9+G|jI$aM7Ni;0NPm#s? zh7e-=&t7;Fy6 z!>liQgXS?XIkI#jv0+!sKP)v(D$y2{Xb+P*ECLO>&eVX_nO_AY&w=Syf^y-Q~2r zb3Cab>r7{I0DdK|^f4+oG8pw{p_W-^1Zszvkb`N`#1SvOwkum?owtP?AP!mf2)#f0 z3}a;yJj+B5G~H^}xgXs#L=LtWa+Y;&MQ>!*a5s9^xflq2n-IaQ^96>2$iqCmL7Ox( zEkLpe(1MJ#9Tq(5j`gIAEUQV9?H7(%*6}@QkH%!3HVo3oa#o%5gZ61d+&w>sZrp2qJD-h zJ(VWQ!+!f8p;ka%IY+Y2tB8y_i^->)jQIwzqw!pWY=9=|_5ix=26`W)Kj&Jb)P3^v z^6#kN?C66d{y3OOY=YISMJ}kK<+-4p!&PaUhe;yqiW;k6q9lYEt8|?2yHG9X1@%=C z=<_{L)~aZ+HXvGv{<3)$v%)U$8`bGeV@D zcd>eH=gqp9kuS|sql2`}wg$61KZUf`w*AhHq6!wNu?VKHgV#LG@%VPz%-JvTvZpz3 z3gKytzkHc*9wS-7bKJBP_n+pYG0n%S-8N_D)r?MQ4s&$=fdjpxvVN@HmYi9I7c=|T zjDik*@R1r+ERTTG~OcmZvlVN9dqJ{ zOwRGTyEs$)>$4ZkbH*2i=e9#$l}vhmIH7 zF4Jh~u0*zeGfVd`@*2tO%s~oDB?3sY=49!6|7YKKDKt+($_~d$C%2*m8|&2uXq5lY zvPUwP^z%wGmzy+Ejvl8Qn7BP@dDeua6g!pyHUQXb!FxiOK=lu7m7D4B--X0J`vtn* zZ%f*Ydo58ofOO8VB^(ia_AL4!8_kI!N%eN1EiwKuw7y`A8Wr%!TlU-y?w8rnB<~0Z zzl$YukpzO3Vay7%4mlqe{yWK9P3O&!GVo2($k|JFvt6M*$!h&rE+(t6*L)lT!mlSq z2se3< z_`VX~_iGPjVce4(cJIyh^zO?(tfPzf`u5(WGX=xo0x-$M#f-?HN#{Whavw&5n$cp-wy$Wj z!-GE&pYNaI5@vU)!KXaGya;c=b(Qu?ODXB@-kKbuWbKgPYRKA1RwkToz+$e*x9mwo z7)(=p?23bQ6F6pyj>s8#dCq|LVh)l&l1GcHumFjOm%hdaSMyXB8GbC#cmc0`H*{;J~4 ztg!}l=+O2uUKXB%f{dn*=G)~S+LZEayTB78`d|3;GzEQ|=IQ(6S{ZZX*~wvL&glvv zXTcGHnP^nW>osF8I1iXqV?BRd077*MwJVv|WYWC7DJJx5QBv(Gc^&*>V9$i>ix#(_ zEcs7Oy%X;(_SCo7(|-$Ns3Yhg)UYT~=9XRF-$FqlQ9L09AyO^b<+%yB8frdje&*S0 zdr&mPt#$HUvsmkdm^iB8#;E7@N$l8Y1)$y@KpMT$0E zOMB5XO8e8BQ%m_bAyjKZ?qVhwQRiSR{DJ=bafX+U_%wbH9WnPsH2b#JLNN;lp2se? z%kyYll)BPFDMB8d3vCm%vhy1_OTy;)TgnP{_YCCM;ju^d@;^fmF zDEI4;aeqLnU$zz-$f7xD?w(!?p7Nv|!+ejuYz;^D`@fL~ee~Ro9!IujBIa(*_~WM6 zO#~e4#t+Av69LDYZp^>UzP*)mFU!|dTEL0S0&!Yv6g}3|aumAc5p0zvj{Ss87Kot> z_(ln?g1SSW3g0Nm5aBMjB+D<%(fyYa-RM6BdW~DdV9|xH{>%gAZUyCTACz#>J|94o z_D0ailmLnf;$LFEZ8r^~hgXw+YuDhdiCBZT#*YhcPXruq*MVcR5@anL>n9@ifBpF9 zW5Yziv0?o4F*FhDGBnot7@cU=^pVe&4&PjumzJ!u^kVL%g8+Aoka} zu>a{w0_M%w{z|y^uce`5;W0*^<98+k=iV9r9{lA*?7?4-e-Hj@B5?m#_z* z%H>kI49ew;a=8eXLg_4A5V3nQB6dq>OY4AK&f%`i#n889_+}5s?xE|zbM)Wf%eX-; z-$BbOXn93j9z@F;@#(m6eLQ!eHhB<3aN3BLgGo8f(06{G70sn5 zped0v18%}Yal1srq}JD>TZfzD8~;#<86Q_ZZ6)0MkiKuKsr}>*)SPNcvvAyCFMA%j z8ny|T$G#nMeHKt8Y9IPJ)U7b0bm+tbv7xVmyXtQ(QT^bEeVkqcMO~m(hR_MEf`K8f zbS=GE*XeG(sQaa2i;pNGwnxl$4svk2C8E9>GY8m3-ZffLir1m^UsF+C@?2`3WS4(L zUpgNV&0~b^sb*12O4ODSrx&veOHRtIFog=evCez7T9q@El?rz0F)kvJkw(Pf#0oP{ zV}zjZ|AsyQHLvXiD+DHmN+WsNHVnF!o&`qlWkU!)hDYmOc*>uuwfA#XPnl zbVta`ix@AsQ!fD~gv1OFbM9>dief|$s&$I~I%-ymPg3b?bVFiKSwGa4S(%Fz+fwv@ zd0$Q*t$G`j3gb-cZtZ1Me2JNlx1`+4%kcMf;AT~C*(~XU&^QMSs9Rwf6;x@Dc;%AA zj4wI2{w=C>I~69}$91J253IBxt*5`!GRr?4u8X{(7ME2(ZX_-X|Od6 z&C2wDc~mS6DCI`MN67W?d_x@*A0NZ)31>yC>6DL;CP0dIp&o!7ygyd!ekDt&nFbkQ zoh5Pt3{(g+ixhhAvn;RR-BV_6Jw#BNbfc6OpDzZH0PBXGaViq?XSO%uBQN$n);J zgt^woA)&yYigIM0DI{2xyr+^eq-LAOdt^CFGZ0=%D|zoTJus_dT?HjuKch)WDKU9W zC%3^o=uOl^x+@;?cLhkpU8j$MG}NjzVs7onZLG5|l=lkLdLLpbo(6;9LVB&yu=AT& zBx?vf6OmdirSFPA4{!;dGtp?>+Dk|3Q;w`9u-**Q0jwB~5+kJ_U=}d}+br24+Sp^| z5BAsbU%HOLGetR%2KG2|A)KWU&hqC$s`sEw7Ceyfb}&-Io)7L97B7&~NMV;&q2}g$ zrUJDa%r~_*Q-it5E?ouO?U6Hq5L-_(MQq%R6lO+^72(+_zuM|%c5mPc0tSU;7$TZ;pA)~SxaudYtBpGB_*2Of5DAjXjcqn_ai1UEh2!=~xO zrq!aJ?6UdvfyWOv>F(YxWAqTUK#TP;c|hq|MaLhM{X}ftLvF=37#x2OQT0FThu1Pj$t3n{a

tH>vg@e)=hW0>z%b8ecH;(9K@{G37XjEar6OOW&u~7g=%U2 zg0*mL;Qt%={|wDzH+4ztL?L(xrI){KNq957G=8HKh_vQ7O%N0_xpXS6cv>zQJ5h)y z3#ApjX+;z?_r=Aa7E?!S$dv#j_WN;P5%R~@;0K^DhDIUhxwr^Za)e(b80@)zt0(=rMXC54A{V7 zzUua3n>}dian?ZH0i_pMk>}`vkin7C0a7VSA3@(SXi4-rELpL_thB3S`H@+w@R*0= zyr`x9>18_4&=)aZH+v>Zr(lDMKEYhZ_1N9Xg}jpp{BEhc3|z?JXL0$d3i!;qHx@h< z!(28|xhnvJRWdNt3U}g~d7mU@DAkCdGOP1B`fokUkh=A?mXGWUV;F;0knZdT~LGAl& z+p=U1gR#wNj*$#VBMXDMY=pTiECb<60E3Afl8skkbC7&q4w9Dux!%4cn@vIz5|+)e zc_9gTY&N?&HphF(Cc(h_{pwd;)!ow)@+5hGp^y5gUsZj7RloYxud3@BMOmgON(QcW zxMo@7&nt?REH{B8?R?9bS612ESMyFR=O_ymJ-6Y5Kq)JX{BidvJhjR$6(YrQhE^GY zH`UPM&06JM9g1%KuroFK&a0aCCidI_z@yiul)aT*@OHzwaNGaG$2d?ipMSM!Qw-i; zdhNB>n)p-6twnFA)ns1J0cjRIyWlcdLMvwgyM^sMx!-dXqjEV=c%q+ilo#~wTz69^ zqFkU9N0rVK6s1Xp_nwE9A$;#xsw=-oytraJosO29j~V@wLLA$woPS5GOG$2pJrp45eOzR#ue{d(!3WWECAdg&)E`gYR2ZkWZ_@IIV)~TDUHTOC5XGR5VK@5NS>PsYr2BQdvrp z&ZqQEad=6=HHTjo_ho5II*&S9dv~GR%RUoF(z(OYirsmZWTj)NrTiYq2`o3kMkM3g zO_U6La^@abE*rPwn~<*fO4U)E1wc*!=|3@T;6EF#$Kd@KTsLz?n1@P1MO)V4JsWqP zQOCXpB3OGbCf^|1la8WR_CQIeTVs{qX;A!?o4}=3ZejOPcHhVDx3T+vb{}W=+u?3h zj-Z~D;|$LohH`aoR*mNdAX}B|uxzh@G`x>F4@1aui~_F3fP08{R%_J)%xA)Og0WQy z?dPuono`!t^#gF}x#c!14RAjTZud%H$O;Vzp9KPHc6`^An!TBabyOz{;>J`jz!HM% z=YZtb+fL5M`L0l0N|y5_rn!o80H8o$zw$~d!_(o~41o{9g{n+CO!Qqq`w(24IlW_P znkgCT&5Si-KKMP!OaBabFTk}Lt~Yb7@HD~*YQ(!dh5n0Zgj%_aIqqcld*EJqlob}~ zp?faK&wT)^U+|eyeI%q`NPie;ABAg# z)3acg(0G_=mvZJNl*Wy4{aT^c#9|u8a-DWwBt=>1ndIl39aJExR`oc`Lmm4DAXVOu zMIiIQa-DW>M5{0+a{euzb9_p($Rt4QldVk+=_pUcJD47@Cd~a4QvRk zd=ts!UXqI%l8eISviHxEON6riO-NF#MBJTNF&@NpdBXZ9F06n0>j*0%W$*Nf@1ME& z6l{lkxP=mF!#~e#`5N0$D)W`UXW%YKx*KusY+|}_Wu#CU%<5=IgC96Ia#+DyjktHoF}I1&N0JNHd(-X4t@tU z<}9qVXS33`+&x$Z2jTVLx6yAa)?SPGT!?!7kL5ITYzD`{Z;9AfGF`FUWf=QTmhS_Y ztUw>aa$C{+E>?K|9i%d!Sx?J-8RQ&W_;scQmitST`r9dKjD3oczk}otl#|Ty3^*u( z$bRiYv}JMT^90(qjhIwXB9u-Bm0)4+N2n*E4a+W#DeHjrGbj(W@`R!2u|;V|89jQ3 zviF9U(C(?R1Kk!ViaLgmJtxLNJ6e@WG7Ww4SDOx;1Vn}tSN?#|P``3n&DKPFCW>M) z*A{$w2Cqw|5u#xFH!0M?z8|hD;W}55T_>J|Qk&1%k5dU1zdyfnA8$Jt;c z%C7S;xw|N~2&AC5e9NHZ_w7Wmwzx-R&0BG(FD6qN2shaP9@JvW-(#YkH z=2K+MHP*s8pN1|qj}BMmvsm+t$`3o3jfpEINKO)M%wlsBD$U=)wH&UG!SyD%w!(E2 zTt&Dh6l%VoMRkZV3@V?r^94P3IKOy1(~h+B38(6_6x26=%oB>CAKKmXkivC8?c6H_ zp5y{g`UJixpJZy1b}xC2X%pFy+n-Cp#aP4t#0@J@M~r+m$!b*EjiZ8}jeRMgLDCEoWlA>^xdazjqMPa64*)I2^d)Vlg}yi| zz=27|kDd`OvJEIcmHNfloL#Guxj2WLhgb!wh;u^t z($0I;F;v*KSE0OOM0?nMi{f>Z($1ZxtyTUAQP$;wLfVyfcVn)8j=^*yRDobt(ohL1 zzrw&jL*tVmzaA9N6Qlo(;z@w#euFD6NM85paAht;Pchp9v2HEwWe4l$i}H(lV-ghQU05`?c40@Vfd;WrT%9Sf9-s<9wtGIqxt9qf~R2S zG^sZ#KS%|sT|mw1wnv+Tv>(YZ^CZX|4;w}B_nMDhzJ$yX z2{CQcA05L$)bFFrEYqLtc^EorTXr%sM=9!Cr`@4fplsXhcx4tA5?PeAy9KE3kKqRW zgT*LS)W!kDxG z7Oru)egc;c;lBW;EnEgUe;FVu??>@6h)0xxI+HR$ zsLIc#C<7guYDH;g?Fv+X(XP1IMuHoMd+q`@tKoVlcs_w&=>%8@0qO{QCdz_E3C z2*tXnKuKftmB3iF^~%5Dm$qI$k-UD3BEB2F^v>#m#n{6e!+XBkKJXxZuqSh=T$-9R z)BPfbo&c7HZL|;AiK^!7`N-}A-SJSL(Xj0IhqV;}S^Z`uwJ3z1#?R_m`fWUOWfPrrD zdf7nu7zQ#(Xnmi>*h%tuJBB>WWEIc%BXfKL|05v%77DWuBggei*J0f%ckujU#CIc7 zfe2^6g4tUCG-QQ3BBr|@!(_lQ`V!U*)+ChMdHI8%2_o(@5j0Ok3mYuOE8D_wR>SUs zLY}`o251KWJ_lttn`qxd8fr%#q^%L{ACJlI0Q*)^L7|G3m+*5D)oOeVphJFkcS~Dr^1l zGCb6+#GntmX@*$AZ{k?_Ap8yHhhyd2;csyENU39>MtucMxgMThX;SRt!-Ox0f5k5d zRcFs6e3jrc6-(68x=~BiRNbsmnblOotd%nS-7FR84f)4_|Ne&ZG`|hIPxBTP5P97Xs4=;qI z;E?X!aP32eUBoeWq(*t|BBpt8FDEzSS^)P7ax=DV;KrA{75!;mH*3O&NO2U;WduIn zK7#EPw=DgAX!^bJ-7OgODOHMS++__6CX*NRk@2;Rgeetja-=rCj!`kaQd5Auo{=%x zpesO#L2GiPmgKn0>v2~^;;xLwT^7L2SC7&ts#ynU2LwwB#gGyKBr}X;*Qd=0ijtI(VVMCe zyDqav9abVJJW^zaWe25ags~EJSZ2MrSVbr+q_q>+PGBSYhPM8H)Yer#ZKY--;cqr- zjARCz4I+}tVv7;f+EqTSB}B6x(XY1>(g+~>w0D(Hdk=GfdQc+@GZDb_Y4Iw*789sZ zkC}+VGy|AIlj-|sYh*j(nPSnN>;b#}d3gRUTp#hZCuGs3JbAO?k)=?0`{H@XobiUn z^AO4N+KOPqljP63o|o6oY!eL+Z;94p!;|0OZ*eLZ<|7bcyr;hIZiuW2z0q~IFQO)3 z-R-HbyBi|wL2q>3?W?N?Ufu1fue%!}txa!q-R+C8HdJ?e>g(=?NPE*8U3dHH>`fPx zo~*k)ftF@NolWnpYijy}HeJ-&osy*WzeNw!)t*30vmxC6_0}~seWCWx zS4Vq-EzO2-6WCkV()9UTKq;<3OS2zan#I`CyrHdsO>LE$x;oIxtd6kXtD~Bk)j|6$ zy4vIsz%%OM>f4yrLEA05>f{l?OVs1lH7~0}t)S@YbDsd9Sr1s(wyX}efo-B)!EfCy zyeWyE{|$Z(`PTqI5H<(Y_1tV-(JI%eigo)cRvV;GQTSjIN<~gC3Gx~dVSxFRg^wHZ zIZ^nm4f)IwKI=k0bA`|Pkk35f(-ZRP54M5v`hDkD zYOM0B4sWa)#~Cv5&^SX~d4@j+;S2XM;BK*xTYO6vSVB{(ZX_!I2hbC_BWzrD5ML9cgFm6Dh{nKf$K90&DFk7p~a6N zUIhn~p%6;RCEe6jNjlFuml(Dcv#rBattM?NU(NE>KS}2!&QK}C%Gl!1WG*XvLnA!@ z`1JZfY_u`yUeBhdr7YC(lRb@~;nO6Wbd}S>)K^NnE>Iywhj#bj+7jw6CE3uuqZql$ z@7bWR(M~UhQKXlIQA*L9Y3t)>GOLiD02{l7y|&=%R`#00*KO>zSx}4cmP+Wk%I`XG z!i@pwIWN9m@B-?2FSgz$aV&UEn|eFNHYxUO5xY~w?hvut@r{ycPN2oNHN8mj4T{fD z34jG5gi=yK6lM01h<#@rgz!7IR0bxc$=8x`r5HNjuo5%25~Z}L z{dF*+(J(}#;jgcyW|;<`)RQTt(4RtCo`WqJGbu9+Uw~>B@ES-kB7BYGR zy$cz+kq!pNe#3GckL75rk0DE`v2KPhegi!XU+hLY8*f;T8C#D=`WwE|X{5{Hi{41D z!xz1wjt9o)>tAsX&$;1sGh#d`JT58L^{7=#$F)7fI$zr}GGvW!VtOc+r5qVQoe})Q z&1W#37li)t%}DT^)$SRjY05D!wbn?Q`on3e2Gdmg^U~DcNSdnQG}VG>>hES_px?yR zmuxH!YNTBIvIHEspbuB0K2O7>$`cAL$UFr?O~Ui;CfXm+56_sTC1WWqozH4CB&R$7 zgDvmZ4eH(6+7Z$=Tbro-7FRv-HIBM7;l`U}_JwdxRWZf(3gPf=(oG#1%AdD&uFe^rrXOM-&rk(8WIvh7yW6);KMdB|1#D`}6j zV7ZLWz;gVguV7ar;r)s?aICvsOn|u!a|7na6Q<$YSrIPV#1v&~L!5u-Wp%voyc$s1 zZl<#B&uX5Yva&s;v9iUra+iBn<2c9g+o~!bA&+T=s_K~saOJD2FdyJlg>jkm)+dfk z%X2C_uf`?_r&JhLWBID*$_x*wv4ZC;qN~kw4RzzZSK6ER3Y&%i-*6sFncFwA9?$JK zQtzJxs)$LTO(CC?gwN?ApKjrEM##qzKAS^63x&^_A)iIUXG_RuvGCa%@>wE$u<`Zw zwRp=S<+K#lc|E)jVvf${QgAKsJK)+2*Hdue-b36EfxDnJnx0`h0WN0>YB|q1OT~-^ z+b`fet&W`q30QZJSLH0{y-qHA3xwPT@-l8gz<7Lz!F6bTPZuS*jF;pxsX=^;#3r`M zfu{^NI`m%StqQl;suSlf->eKvT1;cpPp|m4B?M*C6qLzRJpZ$}EcS94p=^ROQR`$9 z^JH?ikcs|!GRgU6g7NsyjTy)!KAlXIDP(f?lrqtk%j38p0^5d+osoMru#E>dYT0*z zcnC87KDe;`+z)OC($@irzD=Qn5l&WVd8`h!{{_#Z@Vpu6SoYv!}E7=JrA^Y-rQUR?j48?&yO|Hp$n(QsGG46t{o8eu_kJkegV(#V-47I z3^G?L^XWbJ!OL)Zk3VrM1)koJ&9LUdy#+UE%q7AJKuC;lN0XZ@6{?3HUYm@0J3un- z@6kYS0e&=-=xb063h-?Va4W=M8zd6&sR`e@xvp%>q}?)^%bL{sW!w*AxH)2*V#Ym+ z%?xnt%OJZ2M7$S6eGx;QM1-4gXYyJUBJIe?xFZg>Ts26L}A!T(V+Urkf5Ame6`^-e%4wb=3g{BYJNck%Mar9vlCj{wQD zi2NfYqr4LE3FwKjiTwTn{eFlwh;5pn9ze$Z5Ry?_KtHA~w{{AA5Ch|e2%jX30-FA$XkJ~c`_g)lu`QsLleYq^{ItIdg#JKepFX52tIpmSXkWLQL zYZ2kt%Mh}QDJf(lN&8bW?oW{HvXTL^DJ)@@mp^*1XM(W^zm@AhdOUs1+ZF=)4+OL1 z_MQ8xw|gYx-uJS%%O^#In_rfd6KdvWjNe9HmtfdGAR$SFtuNzLJ0P;nCpr;VW2j?H z>Zyrr8t%~b$Tq{%#*c+#%3$X(ynU_(`y$rxkljqwk~M5s=o7;ej?ZX{QQRsBLLVz>|N11ZcREh}vwGosGoSo3LhpuOivFSX%xpxBU35 z>(@A<9~8ajguXG+>dcyYF9X_l+yaT;Z#6Y)QHS0>V${kl@Hdz;61AkBXdi};V(1zI z87iCNk#XIUWfVsAT6qQh4W{%2xD$opTGA*Y^)=-+#iJIHnjbbwrIDee4oLqE{ya}W z%frVlcv)bs`Qp>9V)3a$1l+M@7W?|VE&`Aiyyi0rU*{$PpHm)+;a-pX!%CgQ1{7N= zp_=Uj6SdU%fQ4(a`rxw|#}3D<7F!0!2t$l=kwuHPe54C3TB7EW3vsDe=J!$v;3 zL+EI!K7_X(J(_HYTA^bx(JR+>TT{ehpV4>F~lA zB(d*{{u!;S3)jQqgJ(BkzkG%0mtzZ4DzN@X*K;cc8SOiCLuf(WDnUMv4Y_ghYC%T3 zURuh@YXliBU}+=&6mSLk0zY}JAYbSwuM^~p{N(k5e6gS0Bgj=hd4nML`^mk6Jm4qy z3Gxo{t)j7We(+C?!Kca}Iz+!54N$J`+!E?vY0ypY11k0|u*dhJLI<>62JcVvfr}gA z`9GnyKcmnf2Ag; z!xwnULi#o|g}%k6)i-^n`j+GRR`WiBr_wjYuWvP>Z}ED4LqQQ zZ?&Ml4F&aWaB6*16+9#LUlcs|%wU}__T35BsS2I753WP^+Z#WhWQi z+2iMBzMr2(;qXbkyraXM|2b+R~*3Ggk^OpDV&jaZ!{~oIDeyFzZUBpGrHCA5Kt-1qzmYL03jh zEZAw4!k7YEeAq@}gRYF=r_qq76uc&khLj>#uN3(xr6@!xMO(B|WS1tCdC*?BvHnoj z&Pq+#2@V`Fd|0JJcIIkSNpYYS_lx4;JiBq+vx$z{-2(nNa{3ZH{|%mhf^aSHJ_N1w zFg)*r=a~@pgYf(>@c%Rp_|j-CJ(BcDsig`vCYWw&UGz$%sRL~ zhGV8u{%+tm8-uuK?OV<4wj;&tOLji10X$(%RI~8OH3k5d9DaJPHIn3rQu~ex!XD zBs|)6t#{he=nnBppFml6JB}oi9QwYAB=@|(EgOdxEUQC{l{G*YM1oDSY?kw=P3K4B zLKQsZaXAV?@jeGFci(ILj4n}|Q@l2(e6?AaMk!5Q2vet`v812j^JB%6WPS-rWrdSs zR8vw}q40w$E%$q9s_zL@Mak6gGc_bNik;zS$8by^4rkg#Oxc9tuV3}fQnTFcNVu;F z8-!pppnDMUU@M^URv^;wOUcB8c31(0JW^B&x5QX0sZVGL=&HHPJjIys-j2qY~$aB7V~X|LR7i z`Fb8pub5b|q93@LaXNo~4mBQZ^@FC@=N&wkO)e0!u$Rj|^#>Ql9VO=&<+GY_!m398 zN&Gm<3qSms6!0@@DYXba@4|4+z|ngg@Au9^i_zk+Ky{soJkw73EKlB=GIr+t+Qu-%N;I#2G>TL3;l$uOcwb{U1%ANZz=WDFh~xDJs!;J+ zm$o3!*CS#R5eq(I_Z0kRq=!BzmW28Q6r=cOg8bzgzT?!f7`JA)fbXzx#a_X4(Q>{v z;l$Gfk>7a@$vdX0&mUz=5{oDy8- zUB`9iMvm$H3LZ+0Rep(wAH)tkY_Ws<@lh#3E!7sza-gy)I#0!(cS5-NjzZ^1P5*lj zQ}M>xH8xBzyOW)X|9Utda5DpYiE#qH*_sf&L+$C6B*nfy2=|n&?DfEzYKFPV_$DQqGs% z7qR@*u|xQYRVAH1aEMCrL3}Wu!|K>W@bLT}Vje#4q~GPeW2nPN{VwBZv!*vgB6O{K z*z*W*@x|oK4%ePPs^_a#o%Ohx%kgGeHmv)Xa({&b15+$cWL@Z#`*mC|6Yq-!rk2VI z^ZO`9U&%VODfT-)&G9&$KusTTIVdP8em-Vbp) z>esle00(|ow4NQ;y7cj>;f)IY1O!g;4YMjrbu&kWn`322<1%sQ2@?FG zUcTPMnRNE&=y+!Si%$6n42u8{B9D2UTFR1nxSHUK zl=3K9u5>0n5}uAGZ0)F##~N}et?uAI<%Ff}E@h*07);kuAD_2qd@k|o=?vn`7C25Arx?U3nL(hE z6+|i}1=ON2R4En0N{6tTLs%^WYe^keCWK{&uv$Y{S%I~z4(o&v)~pa#E`*gASS#wV z3L&hv5LSB#t3zO|s>A9GVa*O<6+>7hfwiU%s~o~QF@!ZIgf(|+)hf*kVRePD=7+Er zOs!s}N(k$u5LR~x%b7;M7KX4Eg|HTfu$D}tUrR$+%R*SoLs%=O(XW*utW_bb)gi1k z)99BQ!de@`S{K4vKaGC%gs?V*uzEvSebeaI#t_!YA*@qESf@^-U#EqzHifWG4`H1# zjeczoVVxPm+7iOrI*op93t^oV!a6&IbH8S-9Q z$NQQ{-q%L*zAlpYo*D99T*v#$NZz|6d0!RD`|26;URcL_B$D^#k-T?B^1fn*yu0go z4@L5>Me-hwvuJhcYh@Bfk@sv8m`|LMDo5clJ`ZC zyf1FJexDo3`@Bfr=STA1-f;ci7RmdpNZw~h@;;~G`n@@l_nDErw?y*Z+Hn0oEt2=9 zNZzMM@;;;C`n@rd_sNmGPl@DxYQy!rCzAJuNZ!4Xy!#ri-)qI}ZQA3GT=U9&PmSu>pU{h)xA*CLFf&2bYD?e5&CN~qDB-x}`OukzFE z^hAD=W|blTNMtkhiLQJ#6*Jg4%%p(@kf9(s46+6jS+q62>#_)$Op*70;cwI4D~^-+ zuoyoQ!%Qc?O2bU}^v-A_ZaLycQ`(06Av{r}*n_+LX+gK)zT*{snw<)~p&asX72s-{ zU?Y=2Bqg2%FH&@PD^O*Cw>GLSlMuTdVs*gP3D@iiHbx3W5xYag?i8_S%h*MT1-8US z(_y2fU<0;57XT$wAe1ZtQA$b-Fpf6bLK{t=jh2p$AftywNlTPwiPGXj$zWU?Z>@OC zPOvdY^pGgFL}`^MSs%&?7OQI<=T6+V=e7I9b~@h95c+bOMA;-!PM0WW_)s=u z+%xgE1#er0zHF8#XG)YU5@o9oWgEsl3vXxR?Hr*m+a$_a66I`(a*hw>T#S1j-pKhUizLd$K9nlP#rC%}fVUk&U#b$N zU!n|1lpQ{lOEB)Gc)JX5gF;^}ktml+l*=T_pbupTb3ZX9}66JDQu z@U}viSE5`mQAQ=o4L+28823iJ-GsNBg}&^Q zC^t%!nXdKsCqwlYOw(Hbba=o{2X}EW`tAC}qSQ;K&7k+&F*!pk2AiOxjmxz}# z>^;3Q{p#kH7l;yk{a$#0oP!HKJ#LlK3GeuiMTn$KL%N`8fI-U ztEt8=TjL*&wr1|44@)Yedm2l}E-R+hlFgcK4AoNc1h58HjIrLV8H4!KnhDY~TN~u8 zV~IiDpB)l2z4$O8abA*Gnhwq|O+g*hV!J>#F@30(R3QzN8V!;NDdeBBkWAA^3=(%E zfvKb<2=N#uORtpA1HfVS|D)|nz^tgQbiJy2`$E5Nc&}-eCW=^gH7dBIK*g1y7!^s3 zfMR!RL|}y1RznhPy3@uaia~=5C>T%{SK^ZBxT2y=B40ErI_67^b~KsI8D}#2CX@Nb zZ-hDjIk%Sg`gPOYkp8~cwcK0ho_n@??mc(GS%lSAL8SYjq;N99C*}#Z6wByQ#U`Q` z*d*0%1!w${nJ|m6aGw*~cnN6U3t#h&5sJ0ECkiIlwhV`FKrJ1OwPbW600Sp^%0^Y} zzs>~12#aJss+dGlmXE6TDB0Sg#4D~QAqZia%2E-L~Qdvfmj6+`O4an-z zlL$vR0;#apdT9aM8CS?<{gCyA?HUPDOFByGVkTO%dQ25ZI)RM_fpx;e+VDsh!xn@- zee+D<^h-2+QPKk zGDtbBXpf_CReZ*_Wzk5~p|wZ*0(uFK9t~qgvZc(R3+0ruzyt*%m`^P7tdLUiJ5#|L z4Dg!$havGgYM3ppWQ53Wwxso#!Xjrb%1{NeNhvSU24yn*R+|JOXZlVJQ!wUF>rg=Yk#H6md=v9u&ymt%=pZxRm{LOyihR9r}}@u!2?k+Z%ZiOma=?X%JOXu<=a}8Z);h;ZC*al zC$S>^?qOo6O@NHhCP0&tyfPfISS%4xdH|&yGVG(Nd zXx!W!#q!wA1|HVcMB?@H(kwPvd!v1dGEi&9ZcaT%+36npDZ|(Knn*Dtme08|wCdO1 zt*pkJ{VYpwvaDu#w?lgd+MseHSEC!c*{5}=iE#74W{&06SwjDKPn6dOX-CR^;>KFF z+4q_L#tBd|*$Y-u&hk?>RHV%GQ#Mwle92F_ry^yRp8_p!tm)b0W$4-LF{6H7&%Nw< zjG=n{BVqZ4us{8#s@IzX)vH$9H)@Zr>9tzHnngs!^&Gh_wa!&9X42Kx?1F4WHC~C$ zS2gi_3ro|A-@nt1+KBb#gqCVP(H9>o!q}@|>U&ioaRs}`u!ZCHeYV#WuJ0Y2asNc^ zNTSq14T-xfqqSR$;Nr|-7YFhm;dLYPwdJpAV@G1{1o>Vk49MGl*V=bJlnVC1IB|F| zj4lP_zTNkQ}I_i4xw>75VKhm&Vlv8^ww2Ib{YgQ79SCwPMsg5L2@5iQjP2rS$wd%dmr zE7TR}^1l>`Ybon&QkzNY#ToR4)Z5_Gp32Vg8XsdX5o7)E5aD3pj2Rqec$21;m10=u z5Y*`yl2m7R?Y1wKG76P1>ZIU|Q>^^(BU(*{^O>Ycp+NXY8bKbMo-mZ@9 zS`aqqA$}tNbnMKa&PAJwk8W=&<4v$z4P#^ZxcpY(^4s#GP87!#Cx1?fFbqvhi3h;J ziHdT)<-9?fl7qji#tuCUP~0GzTbyS=YmV{ z78MJ2+gJ7RVz#U5Zx6A8G4eS1K!}sfZ0A~#_mn=B79L%S#0`pj7qFAXq$)7NKA}1)NGHDF$&&z$1m$WCDiW?bbL{)X()WGR^QS^0Ur-a z9=;aT+23bsNaKNuLM9W4>(yt!sxK&fz7U39iI?8_D3sN-wPI7DX$--6SmVdOFLZ7d8!;-6;x|Ad1I5ut8*1zCVvdwTI*uMRh_-C!w!+;F*ae3 z!zwQH+_q4c~ph2N4;^iS_Mw*nQn|UM=iB355~4C)6M9C z(sO2zu5%A7ls-~QCLN$OZ%mK*g!a4;^Z5jF;KawdK8Kqpsn+X#TfHfLX8IsiyYrGA zQ0qaZQ4dyhc2oAZ>57pV{WDoc~_0^nAx(9n<5s>)_Ryr|rdvb2O}W;>da@oOvV ztXgaMTgTJ2KFmp}_m@@OepR*JLX+oxyzVDJX?5)*Bev$X*DD9W1GGOjp!=MV*T8Q^ zjVc`tH^BE#6zzQ+9#%L!JP;1So|!|WDRsxLW&sQD>*>S50X-3Qt>Q;>jT-C<8XKdQ zmgb{MP40VW%(-vH@aKB)D{Wg@In`}abKavES-HO8nFdz5esn&8?{fGKcclfeje5r= z(Ag(MXAfkz`|m?A#7hTj{|^9X;b7tP-^XJ(mmMNFEr+OHt%nFs+aZFpAb@lHu;6rG z?zOMO{Qio;ecvB{tgnaL_apj?5m&$ozVBF$cwDU1L6;b{?=cNWB^*g`c)1Cp1t%tF zVr9cZ`kjtpA-aBxu2?;SUdQ-|>;Qf?-TzsW(}(xK&2KDDX;%S&7oC65WDICYM2pmF z)^ftSR@1vmd^Nh-`D%7u!&j>tt$cz%{f@PHycU~=Q?(`l^{lfjymuc?2Xn6~k=jo- zy_1`=4$jsx8c&+R9a0GzV7&$(VmslcI{M_B^oe^5{cE@-PvprY3htxyv^7zz)jEOI znx%y4)G7K2(OL&YlU%^BFf9B{_n80}9;S3&!bbHw(qwoRs3FE5V=#Cn zdZ#IV#*xJ)VT)00H7L1yU+C0?ooczr&S1HSlfbE3yMwYMwltS=^k&oUyOa>`s7aJ< zT0(Mu`dH2Xdp;sR@DV=Im}8ULYPvtF-&lDtk{9M8-E_Llw$2W@tdNWFwo_Q!DV*(G zNtb$SM^sD2v%T4j+t?myPgG*+aW@(E-4-v_vVvz!`)5x^y@=L~B%duelHQ+4zgZ=JWZ??YjMHQC8MD8yfSn*cpzH^cv}S6)|WkliR%!`5C_4#xAr01 z$A)qN9aAo#W2*_M2pm?tIqwJ7don#oNkFer$}|q?TOez7{G3%L6hmIB4fqVp@UBn- z{qLVE0iK5K_P-(>#_zaP1CHG(XTuk=o?Nv;6sM}Pg1JCL53v9Q}8}c?qL;dpcaUL1` zNcB13Um%nM5}}`@Ti-wcrB?N(-|>BmvF^p!P%W16kQ-;3q&nq~*)tozZq3pler58j zI`X%Y9AAUiYw&Mh0sl*v=~`;C>}#9m=$QQo>qweM27Oj+Q2ogbL4UG0Gs*p1AC+)J z=CuiLyrE_C+@IHnd8R@4Ptc9GNR3h}3Wlcl;JN9b;nfTuUb&~&{nG%ydcgbX0PqIk z`6+lcgMv4h`aT)pR|LE#4+*@Qa(M7>(S{$7Gdz@B!^q*lb)LbZ$(G#c?-f!L04Qz$ zy89{mw(}W8G2fy57PSDW=CLA)_Vaz|X0LP&H_~pc9Y=QPUkFha^#LcOh#>P$)JuZm zMqA&6*2s%zF^+I7xe(U3F!FqOTuZ3~NuA^H-4szSwh6%(0mjp=@|G;89UULsCF7LW zXw_p(=u@NHjIjom#^&=vQ-EZ!;XS{~N?=Q$QRCs)bcCwuh)_*6{_sb^_<~n{MI5DU ztYfyqCDh-6yfvSskbcir0$I`<4^X}!zP)exe!NPR>(za(CrxGb*Zgu>&`SHl^)9M% zi+%M_Asnjz;QD)&V;9|j>&f$A`g?t4)Y@TI_7?}#`pWXHYrX#B>Pq^l@j;ADVT=%7 zO{<0xp<^ynwePiS-2WgAabq21m0vQ;tg<3)FjiI;y{s5z8g+C^jTYW)|5#@|^X6v4 z|FSV}?To}s2*6hQVc{3_9lktHi+hr;chZj>2Mzlh;OrR8w?G`{2>Pv~-}VUG0y(YV zDZ@N|iR`X#Kg3Q^G{t^f#@^uEik^U(;s!ZmuXeiW+5I6^(ftYii>|>$iJZw7>->n8 zlA61>uAm&->0f-!Vt;BRCoxGg9NCKI%+p7}C(H7yqoBasm{b{OEPBbtS(rS{ZcWBX z!E3x66H=_}60j2~e*ItJ-rz}!tS40qPdcyCKllHD+1Ql`hydxnsB?xt_MahVF4xWZ z%L)1U%NH2*@6vf`_RKQQRhW7;YWbPy&Zv_K_@u;t_ZbJo64blW@w&Z#=Ipb0r| z^L(g`(*T#$Jnl7hKtdYHnB5msar6bmy=KV5>;!MDfekZYQ{cB_G8{$)ARCd0#%HiD zjEIkq%`|4y{T90ZcSJVx|B!z0j{E4#IY*d#t&;ZXD4;2< zH5CPrM%Hl78sz+XSc)%XA-%Ag6c3O=>D(*iVe96}KiKCB?Bnpk&s1`TIZeMOj3@KF z%UC`Z%)3029#FThQSw9dn-!7cPI~D6H2U49%Vgv3>!=c`kuou@E@tm>FbE-Y|6*iW z#}FK_|1D+C(9QgV8pXH8iJ*M%*G8=_gfnw8%@J@Q!1TJCUe}qmdaY@F8&1SFOlrbY z51zJoPuJt=e(&i!czTkb3f2yFeOz51Ro92r^+CFJKaF{9r5oVTM;HSplCLP|D$8?j z;G#RHdXsG_bAj$Z+!z7I`wD+1%9gGsUVNbF;5?2^RouOr)^1%y^eUXp;~-L1=XMCs zE{%^$mUgIxehsj5^qT}CFT@P36R3k#D3PsCr4^3$TL5T4m%s7JhkLZ%ZENoTkn~#5 zX)2URFCY6b3j5$*Oy4rAN-c0Nr(=5^g}Z%ouQ91t`|ugfM~V1Wmlu~Jqj>gjP-yZX z)i)p8TD)|*=2Odl+txm))%T6st7|$Fv-Dk}WykmEU$)k;z7MA`S-KI-jLO8+nC$1W zm;PC6(fw7V#L=FiNgijrQ@*7tdJ(h8J4#LajyIr4ucrXw zvxvW0h`*hueKsqi!iP6RuKPS2swRVa@4_}t zn03>=+l5isqaIInp2j-Iv=-Jw*|b3SKH`&OqHU<}r)41){Q3QvJr z;VDonJOygyp&`npDQrZ=@N_et9`&AnfTxGO@O7Q(M5#X1e}0HwHBT1Cn|z?8GX}0Z zNEQ=ij7k~?ADqMatAGGE2Lv!B+oLd_yTlfm&b=5{%heiTjjTGrfbj*;2Qk0pgyJh| ze3TIzw!g~NXo4&qLwS!2%hEXY{WykN<{y^JQ~=eJuG+}HQOhv=5V6)kzG^l1_fU{+ zY9gtyZlq$`NM@JF;4Mn}3{UzFr-DGL3+Q(#{g!Gn^Xn8%jC=tf2U~qyK21fsTQJ0C8f$Kl(B=wB8MfJKB9P<_5opu zGoUcdgUmR)TlPev1L%`hGi=`=#@vU{{BdGnv(()lqXuW6pu9|w*Hv)nUON*{shPcZ z*lNVjUfo;pWP(g;IY|^vF41o2PZn;CG*LGOAbl<)_k() zn%72fQ}2$Ho6+thxoL2x%FP&ehTM#Gza%%~+&OYH-d!L!jY)U0x=XpM)Lq(LqwX^9 zX1S}2juRwO`BcGuf?uYJ?n`o0@4g{7qumeWrosJBxf$c?*YV9*w?=O2Vq*nVI-mCH zlP*#_@rd+6mD@8vb2mt(z`D?=}>Gj(ct zg&c(GoK7FpTs|k5-W9?$Cz#$9!ZatC-W9?$H`;pW?&mn>{A~j3&Y|1{FDLIG!~18c%z>fQO70%yehP9;!gRMtdsLFH{EVeN&jB2I zz6e>d@bQ4ErO5{q@xjqj@o%gODqb(iQ6%&E<)*y&^Z$CsLc<@4^LPNyW8^X1AYRg; z8ns25#7huA!JK!!P;g&GIoro;F&QvH-_G?nkSzR#ZM zn`{*?>3#q))vOu;M$FLO!}Uu$C3piTq$CICHy+;=%y0CYkzUm$>kUOs`kDK z7cYgQ=k#4?cnm$jPGM+up&C9x2(^cPchGN#8h*pnNd(LbsgP6Uo!a<<^8>AHmc=|m zyj~QJ^snG9?tn+`z%`^K8sftyu zl!EoN&S<}j86^&%r0Qeq@9i0n`{qZ`*zRs`?*>LX!m%X{ix%Dy%(1|dqw3HMx7OK;ad~dMtJ{-K3n&o z_wYmC%a1x2?o%P(<~`mT#~Enje~D1O^&@sx)#}ShSPwbv*ZxuUQD-FJ-xq5VKk6T~ zdwRsTMBL_lB@IKH$5iaWz;?$bDz^tR(Q!B7+JkTgW3A5MIafi^IlR$@Sc$rQmcu?g zOOIcWvPDUFtSb{``RMg%|MjzKO`6UNVtFiE#+1ovjj_`T#}E4{T&9WRx<%%|OMI0aHr4IJZ_GxOQyhpEy;xtcUZls(mc)5cV zu0G}gc#-5?vM|Fr1>(_n8m*_#DCDiTWd1DrR-e#yTG?7}9_O$&5$Q*!u%u4rXv51X zX)^a0n-Euysd%9?f2Dk^`8*Z-4d$OFl{_Q^-LbV;wb2zWU&XpQQ9Wh&oaLzAbY&}b zyEe{%u@&;CYFI4R+@K2n0|A#&7QyXZTl*lOL%p{CF_{8*;B!#>n{#hnMQkW7P6TT? z2JY+j2dDcvFSawx^&~ydh1Q`BcTI|;1@#y%j3tQ!z&P+c;?Cy}w5IOBFpRF}53siH z!1MJxUl;}+edheXFmyaB>9O-r&|~MpfH5{+q-ZI&=y)D#9xLLU|HwT@B zSm!ct9>5I>A61Os8lS1j(y;Ja<&b~D$egW^`K4WsdS85Y_}Hvh*sKq*u?3v06FjCG zpZb{{750pp~{S!L04K?oR}BOE4k#*O)^#)rWG9f^tqkOYxOY(`0ow ze8Ezv1e4b)&W*~Ytmi4pu-*1lcB5{5(Q{jGwU3F;(vK*fCPTYoSoEKOgj=Xmkt5zd*5BgewUoE{honsLQ}!tRi%6$J z^VUrD_bynoH$=H8mVREoW^b@^UIC}{n%bWxO)zE*C>!A~2DdhG;>2KWVz9>YRUmG3 z{X%Rx%00X+&8FIQoQmEovS9Y=bzR{mUQo%p2r^dRJ*Nqg6<4bW!V>bn(F2wds)C^5->#4qqr!#`51gm3tJ zS1b54$|h^4)wRmH$DqZ=cSfOfzgZ4klktrU%Hf~v!#`5NKXNeeOJ`M}u2oro8s4Wt z%})HinvFdtz+kqTf!FKNrj&{D3RK)zpPN{r>oVrBIhdO`T;K1FL*aA1;jKZA^8VAj zub>R_yzUS3*lGxupY;DAV=qR(f z*4C`e7{Z86z$A|2bo(u8Ki+}Llg_+fsWWxj)!!DK0iJ6Qgr`y17crwlLp53z)k3Zs zu5aqmfpBfVA<*vyzWfF#@Cef>A(!tHHNUNB0t>(%hFL)Y|!C#+7B!n2_oo+m2&-X_7fKJ-TKQueNaNB;yD zdt>j2zz?k)y7bys*OhtQLs|DOVq@$WO?4fW#P^U&f74X9rhbSrWBEul39ZjQ2smoHsWM6*M8BgCoMGx##~*x~VelN)8R2ZYc zt@0%^OeEsC!|YJG^|$<1*Ys4Zpl^*8jjgece?k%Ns&2ml8$*59E6ISL#xdJeJ{81l zPo)QRQPvoki>+xJ>G~V`tui2Cnsy5P9yi3Qeoog<4cQ#*BDx-C@Kkq>yr%4@b)Gdk zJ!g1xky7?nXDL0|EmQ~sznBd7t|ozC#AX-M_0x##zKnn{=I2kK>(4ZqES(}-`ejXm z$XK6+$yHddfnA2NZvZ9>Pm6#y5SpH+NeYdgXE0~>S&pOb1L3Wn9U)npC~Gd(Y)jZW#TzX<|4sLCm3b#J z*Gl5&h?nk=cxuY|BR%Ef)3ESBruC#`1KRl#zD+rA{yzW!0RR8AeFuCT#r6N*-d;b+ zI!RtxuII{HD`8_BSlJX~Y>EMcf#5 zG%X#Ze`Rs!@uucxCmqnV;&^6~ro}6kpP^}ivPddo4w`UlDw0`9z+|*6YL>;)Wt`_S z%47dFQXCOEGS5V{c)B;gIvcOFbb^1)reDR;>Qp+#zo(+G8&JBzfLbJonfu)iSd?MI zk?4Zv=H^Iom<#w53Ml8d5xkAi+qBMrExVeVL$!AhI9<$;e-Xrag#f8&Dz>sHl6jc& z$LgM?|Cui%T6{z*Rxy)m6XN8LA^AH^C@;$8twVW9etQ$%9sz2s=LlRDPM7eD1@vO` znZ&paV#xOE|=9A8YOrHdGTJHhR-*+Z`i%;n5ms?uS8bvd$xM3&lnz*AX^^N{8B z>~p>>wqcGyfjhKtm1>SDqQvTV(;ENY6WN9;PVa`dhCp7kC>>#7DUvL#sAvjIfkfR) zoC?UhX4YyxN&9ps+IswC%8-sT=xqWyAv9);0WXL&<{rO?L;~;#Qk+OBQsF9s957B3 ze|2pN*05k@S00z|} zw}hBCxXTui5;Fd1mEx;`TKFv>rU;Kd|xm2`iOU~zi)C0!@e z^E$deL+~Pco{0`bc*ElEzo+A>gx0e{=F1gPYEC^>yFIR zNNw@k^U*T*axY3cCkvHYo1)a|-57K@vOh@AF`=U3=|K!12lx(37>!k!nIDKN@$_(h zRY|YHwT(bh%n+eh?{QUgQs~v7uexpZ%!hw+rd5y?1&f|H^^}qxN1X_i|Z(WPghQeG{r* z91c(^$n3>{3CI?!9RtPEhd~zb4vZ<2B`k``uZocUR>Bx7j`{TeV1D~GsVnExCS`Au zP9qdvhUH-4T1pvQ_&Cy+@at!h{&@{dudI+vmwo*%dKIodkL_I2xdAEv18vRPeQ!%pZXCiA~sT7cCO?n9b8i}H7h257r zq5|&^j7d@bBWjT1H@l#6=M$>BCkR@Uu3_*eJ7H%}LlF^%yhRZEnRK<3fBF^aJMl@n zv7e9>%G=IH$`a{P2CODPDp4^@D5f_dM|3|4rfmQxScI~#qZhBd=m`^q%7h$(UAfs`sb?cUf50&mZ%-M z!{f#u2;>YWK(1kyQggk?z-sp<H(vN&^)-hG1tXT zV!~^bJG}?P*28w(+=_XWdh~Xr`5tJcf?C02gTs}EUVsD-1lqKp(hMEL_VY1iQVvAc zFChNXUXKyOirU8k+LJ+pkY;osPnE_1U8DeIFOvL_7^)wwa?bvt4A)(M9t?lZ)D`|REl(Hzn$uE$&1 z?Ka}=%x2Uw>a(cQY(0Xvb35WD$WHhRWcYvzYiU-cu0I7S9;4^t;*R)jwVbO8xh_MU zr5dip5GGmM3+$9GN*IQ)gsh12dtg;AjG37iP`uPTAMTVArd%xPpuCL4la=4`)K1~9 z!2M7r=pH1PQ}?2<1(M8DX7p#m%v}YVrR_cgWq$%u-pjLlWPJV) z#Y|w(0Z6lC7uv(`K}yp(6mQp1J;@7`M!p1vF3mCjyUY5Mb#kg;MIG7ycR&Z5`yGVx zq|3Dti7sS2t-PG^{A%qERGb=!!^#kE_vd9J}J^z3}M zE#IM#Z8~Y2H%crnX1li6cF#>6g5382omA|pZQP8%+2%FluC0{l65@&9x9CH%S3?P= zL~?x#1wU89;adbp_Y#f+{;azPYT@igGKkhsh#GU$Sl1(XpNdGxdkc@i)>J(PeN&|e zjZm>s94ZbMM~b7xq?(Eo#mV9#s^ZrqWzfXI(Ftv`EYmGP(}BWUanU-HTmd}&dX;*86eve7|plq^$EZzSr40oE;&-?*+fg||?` zy75Fz!xq5nc9KLjY@^324L;(-q}vY>K4N96()AIY`{*D2hQj+Lwf?WDzn(y}_aZr5 zLYG0XL+O4nY08i2K9r>6D7wrLM+y3oG&j*52CkOwLns{_wQ7Q&2k#Xg5nWReQ6O9k z=z5vpY3ufZR9oa(PS>{@JHJm+GQ=<7@4ZD= zh%PAXv*;Q~J+7LrRRjaG;NLi>gKI^ZMa244@vO35tn4f8JDp#^2RWS}QHEUWO2fJU z%N*{q?Ohb9UNdKz`y3QdVWLXsILOQ@?(UV&AqbIGWCrM@29D9xFt5@Xjry&iY^iXT zoW8l44TNq4yFZn}DYl;`ZJaPu@eDbh@pCepkf1Syfx`UFdm?8%^BKW55&8*)9--lR z*#L|$`xceXt0LA~JGWVlhAN#WL>!px@;#(d>!PIg%3?&q^9ur!1p4t6{Yb?!htW$x zn{_5^k$OAzQeV>RE^RBRS20TSJ49*YvN~@qKNN0)9Hdk_gW>NbML0241g>UA>4g!2 z<7LIHC@Y=Mc8RdI!M{GBUzC8`X8%!5%8FGw&%vj<7VIF7CRUtdoeYi@nS?3MDQ*X* zwd{jcI@cnIyoMUzE+eUf5WuB|dkLeHs+d;_^83hEwm(SqsU9G?ZofhN?UOl=SRxzS ze%jVHvDZ-P97+~0k}}MF`(Wtuwd+4tI-?LUFrq8Y8S^Iljg?M+_(=~UO$7rI1PG1q z<$6w)&UW|^Ck0hVxl)94^?G>v*-z0_UlJiT&$896bY4O72Z$??>^8R{Bi0r0(#%pb z8(v5z?Jh&{KH_72Bs-d31~d0YTQ+tiJ9(kP7R=m-rVh!j<;*~EA;gGcA?gQW5F;gh zJm~ozW0o=oj3huXb3Yu?2EvvllyfsL!HQ1x#b4QlsdRS2u~-&V>V@l_83_HNAkeiN zGlM)3{Yrbkf|&He0d%7yz|&a*U; z?3FHe%pxUY`-Apv8k)k#d~hKegwkANEm*FjbKAyhZ$XTu)=F@&z}%bAvKcV%nULMdD0 zFN3!@oD-ajh{zHC3aK7A9px>iq@qQgVj{p-bJ!sHz|*+()sf?h!}=GnY~ET&Hd_|R6w%Q z`5jXK(dA2K6xDyww^jVB@A4Fyw=7j=iq5qm^r$}L5M#4m$QgRqjg0^MJ6e7OI;O0?#Vie6)4 z>~c7JtKU$Ef|-MGLm^;{qDwf$rv#xMAAAd4m+BlRJ5=-HrPsqFC12R9zFC$?C0wuA z_0Z!XV|JoESzcu8VL3KSkPmqfGV4dceUsdcL-vFT0Nt3@Cy7BwTnTZ>1{VAwicSK*3;y=cMR zaC8xgWCNIR&V{RYaJ0gwqSJ9--&#(8~gI`*UL>G!qj}*s^mpv}d#;mU++jvrlN*bbhKd z)KlNEdg@1tT0L6_Qr)=31eb{E8s~qSn(|3F%MfuB=()^^jS%Mmzs$fHtuco~_;m)I zK%mVJf{VhrW$Te}rU37+;eN|dS3<-R1vjeSVg-@lSmF=Ri(@W#>x&f=IrWX?);HqT zH{#Ye;?_6ft8c_tUxQ100Dfgjvs&Yja8|)_M8K!aFypc6b8Rhq;1w<{u~aOVmRK$= zF_)H@OH0h9CFY|g##z?`yAb;m0U~Q({>s2y+1tqnH&rq53RO(Jk18f! z=@k?AilLhHT1?))Tt)E;{yj#bx_K4+)=-spK!5Ioh*!bITTRzjvhS9WE!$J}QLpcI zWK^%$J*um5bk}h_(FGiwS7FlYp6&r0>w2IrFZ2M8JI(Ins2$+pcqky+IVCi0oD}VT zVKISa6a0R%p}n0G!=<7mYBz&?+LaDnO~q2%KB!$QFc zH5<3r_yRri{*Gi>p(f(?Qs*^Ij*Q}B1&2QG26|tM=`o?9;?5aRi4SX1|Hhr4_?Ki1 zkkuXFHJv{1SHOTtHKD}gVg}1TUZ}9RV~Rr7xk(}dsv$|*ZS{+;r}x%2goE^pP%mpM zKO>*@BR|h0+iLCLXB25|=Vw&V+QrYPjI|TbX-yP#Qxl2@`r7JdIWHM^7D25p5-KY0 zTqKeJd+oT!|K_+zswgnaz92ij$058(J)0UVvGT|L9G8sFJ49GFa;b{0 z4$Q41ix;2o7X44@zWvj4-=g#Vt3p8<45(&k0|sm3cxw&kruQXk2!O6a`S;Df~ciYmtuvaxkbgLHOcdtrS z+%8BkcdiCkK+wNWm-wSsWGbm(-J+o=^XYt5PhHltlr1mlMjieOUxevDMAF~r($Bnk z!=>R>+2XjjD&MxIbdK^7d|?MImkN}Al%zybF;!C9=$87DEcGQ%&fJ>IT$v=PI=x)9 zudKywOh=IDI8w<1m#~kE`D2OsP%@+~z%iD5$FJQhE$pcMa6S%Vxt3 z7BluM#kiEr!60`m(heP@u{%Xas1O}SD|8qx9nee5a{KE*m!mPs(U{l&ZIks8@*>v$ z8i~X>(MleP2a^ZwYNo%UXAk#!_BvaWI%;hJ&7_*GO6jL;wxxdR{Y=Zz z!(M%7Ni4H`^_5!VuT;-{WSv{owOVrSEni;{b@pi?=ElR&5a`2dy+CLC0!p^dBpDdt zlNxD%rptQCRPP~`5R-T~jq=Bqc=o<-G7&z%K6nNHW>-(O6mv7tu4%7Xk$J=g|^8uHG z`cLd2o;(?5Je#5znr>G9NJnWZh^x`7dqKP3e%&fEqgv@#+#8Z!R@I9TnMUT1d^4mu zt9}FW-m;x)2FWh;n61G7h{~DkxP1ruRf1^PzffITlT1_8prC`K^iNHVr+>OQCaT|6 zzpM`9bL8ocBiEf7}GE+K>*?kg4Nf4}3_> zeI7w$`nEhWnd?&`AlaCRhwJtwKF&`>3Jl<*B6c9>X{xHUs9Hl>HAV}5C*ueN(~fMl z%zQ13vV)n=sPLS!eM(=IH)f1i-k9-R9(V=obrh8socD#U>G5!NfB)DA_`_VolgPKK z%2}fskoOin$ZX5M=x94+L#{3F7M@hl#nWaCF|S>Hc&yJm97Ak1o?v~^VH~a2$NH}i z#{qO-ZL}?$1?b33BM}k(IUcsYEJRZ$dGl2d)a&aW;P3wm@urh?`D+Ml$x(+LkiS-G zV)>h-xv|oLGKbPzUSk+_hhW?@MCs8&I_{gON&%m{V{5(hd!}9w1epW09IX$0Q@epr zw~i+?UTrepMmyMd^V`7l>ja&LYCVJJKo8G^ZqzfF`Ock>fZm)g^yYtAZ%)dYxl71< z8|&8@XP7Az+O>0fvz4~>>AP;>ndI?I`PFJ1`8_0YC>p*V|I0H!U<@apxA>m7DmgH; z-c;@}#4k4()B!Bz4Z{T2zc#e^igRjz3$`7FRbMO48FQ9BnbQU{+oE{AoN5~oI2ckE zGRwmD##qW&nb7NjC_KDkW;{}N5Q*t(UYKQ{ZD)0be>n`Guf!sg5*(ZgrNS$dT$I2o zutZmF)hsy}p&#mZ^}TYEB?n*LbPJpx^2YXRNY+4#vYpYJrj zi#Xi+Rdo(e{&-B_8R5kSW-HmLtHQ>L?P%AEy8T6z2hV0*3eP(kE}nlB5dnxvuwoSE zOuj<^u!fpsKKX*XAT~`=;deHB^txlBein^|a_5vg%L|EC_<%DtvDdUic2=)P^)fcb zd-WhO6ChH|ra-(gDc@DoJKvZ*?8XEqPSmi#LX>%82g)p5mTu|vzeRzCCX+O8zGUBh zy|RCzalE7RWp}R3J@@p=Tu1j~bJi913X}$#deh*xUt}5nH<|b^v}ARWdkn}`Xj?NX zVz-z-K%4}7eNNMSIm==`bSp?zs5V7BCTZViW38Jeo^Nj)&b16?3{+D@ zdXx55jm7@znpLVT$7w%*$g3O93ONpSlXPB0*YT3h{ku%(2uZ|!p0n;UEH3NtOGS4k zB*+%EasH3cw=Rud`>a^u)!`eukHh@akO;QF3A*p13yT|P$hurj*W6s1-Kq_iA~e_& zA+bItVgFvY!bDt6Mo^cqZ@2f*>!AxWuab8%qJo*7uopL;t=B^@WOl={TCp%GA(XGZ zK{ZY!CRZ#b#JVEJT>X%{X(1q9jYpnGsBv}i({6_yEOGf%w0@{o0`@MB3XzH5t1>r^Zuk5{p6-P@h2=0Uv zf;&arre{mWc?>ENM+Dg~ODTv7DKNx~$$~*blqHdTLzTSq-;#e~|~$48w^ zAo#OMGHbF(?dwY{38$dK4@x;W2HQQW-(Ycbup!n~^EF3n=Gh$9>qmb1SV})r><0{` zLhj0EUIQDlb^F2s#%6_V>=j;PQ(i2--7Jo$IT3LR-4V&g+O8j@ zguP8yIl(`sJC3F?YHz{vyf{;^(23BmaLUNc?XQJr$qn2gvGguF9xfEG#}0`qJ8MOk zZNb!J<1=|itp#&Ua#tA;IfrLQ$n@-Yu@62|*IzBr4mn4QbJv6e_hWuFqMT%us78ucdxxAfayafWYayps zlbH1fS89kswU;-k^GQO^cUYZ_6WEBSakwP1ue_?6AMyk~d=yB-tQ_0dkx{E&s#%E` zbU>AqOW4BMxoh*|LGH^^G30jxJE!4;K$#?zi;yLpXzbHk!@G1{E&J@%E&FUl_MK{N z;8gBA7}Jh#S=SEDa}#AnsRvFxSjmBCt^%ppbIULsCg^2xUp;Ed>bV%4kc4N%L@b9p__&`Wi% z0U96gWWSTWL2u@MCq?|bLj6w4_PgA1_FdxB2{^OFH^Vha>NV#+(Fw2cb;6sG@^01V zmb#r#svDe?Bs7d!6}%uGL?<*@8uR+1>@4C8#1*11?#!>6R13Swz9`j3ALJO;td7kK ziL<)=eKFhQ_0uoV7jd?VtJMsv*w-hY)B0~#I$qZ@8vEZqY}Iks)6y0H^UP5D9p)~% zvHh+>8~($JmP*YW<=LB`OIm!r*&}MNw)$`f2zpHR;Ky3_;Fe>QK3e~AsxR|g|M3{l zQ<30gP15IrRu%SYTblt=^A7GjXPX+ZWxPVTH@%36WyH;XJV(%xbzgy-=2>8a! zSIBa%@Y+ibVsyK2WD4{&4roGL1ibwhRt@+lvaTkrSQD4yqYZX>dKW!eGt{zxg#8r3 znq8Q1U+v#Q5ga07X`nH))OB<1WsSY!;dxIJQ8k9Gh37Zsh@f1{O5W!xtrwqN5)$)X zt@Ezcc(>jZz6v4WEu^^1&E9oev6S@CL_72I?XSrww|>d;2!3PT-3elytzJ{!vLo0> z>cP6jSauuET#a=QM<#fi+MaHqX<3=HR~cK&9=RkiHzKCU#fDJ1>8i0LyNF(7@1%cq z-(hu8*yUNJRx(oCA3RHtytU1&#kpV9Ag zu^z>cG9}04I9dbpit#w+KQ@tEt)V;Yho1e<5_J~AUQ#SxwkKu3CMfkqETW4Gn!iY-#+7Td#FBAxlL+u7h!Sw{8Ua(sRo?A(hkM?RCb;4?46A;;^r7-XXU zg|wu{hd1A3KC~alZ&gNv-j+J7t!b~_T4}Fx)n0AWJ9cbG>1}mxOwxOO2kC7`&U$xD z&bm+U4c+6f*$w`7Zq85Ady}9S{p&b-|HpRXpV`FHb~;Y$#4Fk-PGs7<-KG^?Xwxq7 zXr*fx3;G1@uFvrq<5WNehx@$>lTV(497X+$>hxu&FxGv#YFiou)>1h7EH9|*n zenZ1CcqesWY@3^39R0HJa6*zDyHssdj)%=l0kSILK8dZs;R=TI^&ov1($9lbGNiu; z>C2D-9;6>b26_Yjc`u2yCZ1nvtjRuy zPH5dhQt9;uYY1u4_+}iy6p?#ps(M&tFo`R#)G)z@x6^phzE^VL_D1^ zPCM;JEz~nSxU;i7@U5S1DF?k|oxy{NzPf9^x(D34zuS7L;WY98NS|%%>YB=v+Z%gzpnV%o?&#Xi z*4IJJ>@Xdz`|S-gyT8Wtq@Li%NeOq)6+Vt}3T)rN7!F$$=NulP@mUJ1=t5u;artsN z?PkB{mbPBn#q}*CZ@O<2PBw3|O_6FoO?B)d`wDxSJg7#Gnh|SCl(G(|tT+be zT==Ivt?10gSZt_nrG&KaOadsJK%_j6IwRyD@e%u8Y#A z3mOx)W{;(}Q1LNJ@`lFeLO!AK89q2B4spVr8lU-twFTlpYDB%PGj&jcN2cHUk~&aP zOrAOuw(qvf{l_PT?HioyH5KL$+lym0eBeb{*tr-1^*cdDgmHgGsBplQ&hK$1vpq{6 zepGhuXQUaJKPf{nh{v^17Sovmj*o}=sEKPiKpl2^H_P(@_dlyyouLzUn#DJNqA4Si zGOWSG+TmrPRUD8GJ0Ic1!%+2A97GR0e?wwsHvM{`Nsf6N&yyfhd}S%#Q}P9sfm!u| zK3Rk^M&p^O~$Df@U~#3OV{8#QN z*6U{@>@!MslSW(1)+`Imtq|^QGHS~6cB0lTY8xNX;p5#Ml6Q9$3^+{bHx$@SgSw33sw0epVfG;Pd6ZLT`A>rT~HyT_)W9gzHcy2b_gY_0PGd^QZE z?O3}1TehLAlj3>1j*INIvITL37ZYUP>7YG2*Mj0u8NFo-db_D*3kG_w1qDlm96Dbz zOZQ~-4o=1r-%SuYrO0_BuZdoi?YI_A?SC8K6J+~d>$U%KDH1ry-@el?H{9g- zI*FK^$NZ0I%hR=zi&R^lu8p~o+Y&KteAhtiMFT0hd(q?$R?J*2mpp6~JVZ-X_mR8T z{_a}Vd0<$HH&1hl#@6o?q2<{Q5wolZFsBi0I7Nh(PXO(&^%sX!o`~ny@hn&`Py7*J z-D<{i;PnV(mkIX9c_qqHE>y2Sm2GHHuA{|YC+)D=aH>br>pqeMc_LP9h;=BC{g5%6 z#?<>gq7`y2Y}uLpVte7TR>ih)ZYQf?j7l2J-725F7rd2bPsi3)G!QM%S^{P~hgq?X3|5R<)?r2Vk7QdFrhkIgD?1Dz4l>mef6R;pGa z6q!{LG$J!g^l)TmT#vNbRP38O2SPq}=K+ATmk^~Baz&}Ou34TaoD{~`UMTpNbfWYl zWR70XLjh;IP_*uTbnFAiestSvIr`=`NvQX+8jmy%(R}NT-q5Z4sr1koHOumhUE?x% zm=x1tz78XjtDyNcb}|%~!S>5>2vf1a#}S4_yr^q?3W^QlwQ%x6QpT}@=%+3Gw47BS z#_WW25zCA0xG(e|eF)v&jj}>3!XLm<#qSbJvGEm~46c>z!C_$`-*){&mB+E3++W|J zi@H2?QRBC3>=SXspy$MGUdB9@Zrm02z9Bw+F;p7zoxZ62F3chLk1!m2z4Q;o;okOI zK1-{v&^8-bJm;MNr73fb(wmqaf&}S@$YQdBX|*A3~d&)(~?`!!~y{{R?x84s`CSS2;URl zc?1RH5ADyh`tezR9H71{(KGm->;k?v>T5k!2w&&qA_w2oJ&f;}9_HVldKllcJ&bQ% z5952zhwqT?v`4UEws1cf?el8Cn~Sq6GzJ@v_u*WxaQ1}UywJmZe6ff7-Jg57pS;w= z_5Di^<9oS>@x9W+_+IT{e6RH|zSnzLUf$?o`Zn}%|M+VUEl_!d94ndam1M3YI{znld|@9kJ0sxyUx*F=h{i1SbB}`sx_BK z6ZDMyb0^y6yHRN!N`t+)#22=P(on^E50XU^b{M^i2;V)V?H}NAxQibF;()xX_|CwH zh)iIo%R?IPba_VO9YeqNxT!aVza5}H+9k$31o;k-{Qx;mLVj=|GsG}Tj3n@44=h93 zi~ixGK$=x8zPh5<3rzJQcl`{(<>3P{oEHo2#5mL+4;(I+uHm~l_P+jjpcwKy_{_3B zMeZjWE;7$_GuOynM@2}q-dn`$1qTK9MU2vZDrZNU;AOYkJXCC0f0y&MVG-Y-7Qnu2 z@OID)@lCXEa@gw|LobkM7kFsxAyKga5uc&Fj4mvg#z68KO)OLtTa>tHX0f$-0Qg5rscH- zpK{JYCwskMe9{?-1&IR^*)LlU1h%oi6|*5*%L4;A1|Qbe6B%cQyz?l0(V3j64RN0X zEQp6ByvL;XSY&I(vePAsL;R4G_afE8r9lp)U_*LTbjb*J%nuNsY8+T*WQQ+TQAzBf zF;m9sRLHuH1M_A;E{+wkA)5$9Nc>G=3BGl616`XnKHUI|PH`wGrryw*N&|w@Wj`mp zP3BtqFShK~$UP@v`w@GR3IyF#suxZ?U4@3Ov@fBWK#8c3&xL5COI&FzaJX7&fHTO6 zKYX;qUan$9=#!kBma$ic5FE@tV}w^IPe zHx4lu3`VM0Qb+ZD{*H3i_y2V5(8LbnY26N4|4=#~bbcHxB$u2%eIq5P`y}8h_;5HA zh-mSR)`!sWk?LVC6}@Cr^ztbNEn81gRuxu9NeH{QuU24xd58J1S*+|8XA5=kU_jg&Kia}bEU(Ol)IB*Z2~z`9JO?2YJ;)WT+X3`3r~<%IqyPr(lG8 zU~Jec%S}x$=Wc4s zo4G+GZ@N2XwjRZ_k}vwnlLIgIhm4-69_6Pb=JIA-t#f`;ew!m&DUWp&{m5N&+fw#< z&TRL#8h~W`CX(&!>-4W~ILX3%MPKXwm|hPDq&nT!3OQTrU1f^CStRl3b)VxP4C~;5 z-uFz^+<{&pqng8f{(Lq@;6LH!vZ1P^`1m?Tm4LN>Oy&pDzX8tUa&e-owrub= zar!&+j24|4Wt`rYjOLB{{A(uW2mxcF%{iI(!Cm#^L;NtMmJzB&Ox0Imj`>S!-gCtq zvyn2q{r19PHpHqmLlloQ>#%a|NnO>y_GVp;B@Fw0r-6RiM^b9>Ax_GVsK9^ex{

whX#2^Vuv&J2fB6z__GrC267n2d&3R;R_>3!9zW_VUX^Fqi<~4f z3D)w4e8ZW8^@I5OpeZ-f8_o>OCvOS(I`ONLxvzT6HRa}XLkQyLmoRXOGN3}7u!0e0 zZ5-fY28)kVoyKDzLo6DTjKY#J-dk@tA3(G(6r%0PbC};fE3%-cr2l!nq4ClPoL3^b z%nOlj;~|gXT!ZAy2F#wDbM~utmoFL)w#eHvd~8baGqcYpApeiGD}j%yy87?ES>9xp zz>qLANsvS+Fy;jiaJd0wb3qHD#RYfVcO4u=ElP}tLKQazMGzHKKm|nH6?d(5*IKPr z6YEl|*0#3QuT_%po&P=ezV~J_K|u5Sz0AyccfaSJd+ymToyu9iZ}}4Yu*sHb`Lq-i zH67C2l7~3SFnH&S^=M6c);jT#RCLr&BXJ& zUTsc`@V$7h>t2dzQd$#%NvQO~B;eGbU?1hHvm!^nPp+9s?ev zFGm*Oalp8uG#(qpcnthKz+&7|1HGH&A^e|2wI6Fi=U19c=h5XwCR>l6Mi1>ml6!j1 z;0R;QYK*2Cx^xd~%L}kJaQb1L5SQ`OncE3*6=IxG8gUg#-(MnbPZx3b0OFq01>!2) zt15xG3a>gkB;qOz|HhBF3YkB25m#a8D<0xL*`bWzK=+CeS1q(zp%!{qdq5qE2dvM%ZtpfISuri~!VewzGuN;tFxO}6RF!~$Fn z8(&h|Gr>yp2x?az%2YRoRvM+fP_(He~rCZ60|3Zd_Zd3HK? zKH8to{aSY8T>Af^$}q}ZK#d>k+vVdD9Yr6urZ$v4b(y(vnHgpLC^K(gWA@Z`vmi@n z0ViT=D8@x3g@QeNjmfbSRw`-*|ArwZ-;IsyiC(0Kjwi-Os_c4#PsUzs7@hE=@nS=5 zp>b!qfQLjlcM`RFX8z0WhPnxkzkZ6Zy05C|RycRI?0QcApGN<^5$6F><1r4MW~BRt z<~QR4%$_qLFvAQOmeQw)3{noi$3<3Q10^odrtfj_Uktr$WJ9AQY;o}!#&u7(3WZS; zvBmEsXGdST8)u>$o^h}`t=%rmynyl!mlTe%`zmBD+w4x?EQFD2#EOtaI4;gag5P(z zoUjl5a|2~au-qwBW{k$`)WV~Zq>6F5v4(S}p)X670!Rjq0$1WVq-|(rOlyj%| zcF$W|_RsH1K9%2inla7FehVl4oh;>O#P?VkMoAgu*2x<3|F@-b!GB(2NOfFNQ7Fq9 z-$@LDrAt&G+M0>xk0eqZE+W<8nXMI_WJw=-hvBc(mVnPLB^uruj2bolj0~^AsIlv4 zqle}(J~5R3k=q)QanUqQf&&tQn!t$Rbb8J=3J76PhdO=B(bA0{qOb(P?G*I@;)*U(zlwW?JhNNDPMZ#FL zLXSnlxObx-i;xkb>dxN6xuJ%j7?#=8#@hlcg8^6cdH}j&T!|^UN*~tJEqeg)WDm%i*L*X(4K@cth8>YA;47ga}9n-5A3^zV; z6qRWEb`f=?5Z%CW_GG|d@9>S|XI+}XpnMX)+(T(GyEdxB^w_U2K`0R0rmlhk?{z?b zm>op8G<=Mqc6Ne4jfBse9`}B4r&Bk~1nM&U7T!SuIu|NyaG6xDF-6;1inn@jsxv?f znPLJN_4!9KXTvnlw@*h2=3%5Hph~py3seRrr9Q1yl*%%6U0sS_g9b_tQw_ahep>*B z+gZ~kQvWps#8<94^dB$(=!U+e{nPw()CSGvw7FU zF-uN*7ZQjFv#^SF9Mlf6pUs=8&HH4f>VG@Nl{d-`wb2l`t&Q3@a-rQC&bHP#uWq`(@R)0Em-B;^mi z*p41P#iM9wucSxcy#qP(Y@=(v9TwRMzP-gd7d(LMcf*065so4SNskmL1>b{?pOx`4 zTj$n5?kV?wb}GA4%2$6})?dQRF-|BXB*_Xhq`CHqLuE%3; zY+K_FbS~q{pQA5V{`9_F`g6?fr6t<2MY((DcFg%1#`LnEp^VFahB9XV9CJHck7+>0 z^ON=K4@Y)I-5*1Vs-)_?PJto7e`?byzEV%MPr=x@hU%4vmhu`2AO?Q->1 zQNo_x2>?zGg89A%2he~45$q`Q7CY)+X0b6DglsohEZ}cippQUJSL54mFz&lZ3Y8?w zVLL0s=`jd_%KV${+~6E2lQvz0^t)5~rfcy{l)>|o`EFaB1qK-nkk~jt9hfb8Bi+3Gyld!uIX^@k1jpKV)X)Zcwt#|1JWP4= z@0Usy=2Y(>)pe%Z0_n19OPAHL^i(VThiS6ws$=r;88RD3G3B=NdXG@H5!&f;=G$#& zqktxzz{3&pJ{9A^kwc=?0k*T)E5LI4COO!qE*y&!25mUu0Qo+Urb;^N?|X!~!i=7k zr=8u?F+Pyl&izh3B^WN*NFGDywn*+e8q+fQf7runkeuH*FX^F&mwjt_k8nqdI;*%+ zJ(geL#4sH8i+WDpliF`N@sZq!GR+^`Qi~`)b!E!7SO>A~&>o?Ja@D&j?FtDRl4|6{ zUf|~YcIe>Swp5(84yXuv_{cfBJ4U|lT60=5m<`x)$E}Kbrn9XvMr00x1JDNA9->1|2ry(P%j_;1*TaJHXSab$1hK zvd=9Y$FDxj>mHw40bCJJ#@#i|fqAN8skr=>kpF2+`TuH^gT-zqh+5OFcG7C7T*jp)8c-e zMxT$+=S>8AlAfI`wom<@V2E6Qj={Pg#JvYudDmq3?`tXDFR6~<^!X0zr%xsqnAkc{ zs*1Q6O^B9npt3+S@kEDSsV>BDj(r6Egg zLvDyAmvn^lT@I)3aL+L?BH&|kTx!Av#=7*}g=>fFLMk}JSXBtLS$sB+i-9Nv!eh5V zFs{yAbrR&w(PT7LTemD3P1p9N|G7OR<@L8TQkD$WcF&HY@5$iwg+u}>Y&q;nTQ1&k zZB#cR!xe69=SK7bao&MptKGwR6$5T;hY0i%$RYLsMXu|^@QaZUDn7ThRQZ=7qJ#y* zCr6a(K2q1+gR_rObzh9Csu|XRBH_JJ*Pj73s5`$eAxUmNk(A*FxI&V+*xaUsJT#*z z27L|Cf03q=0UxyK{YS?8`2|B?zzNyNA45Hy=V;yOBxjw_#+`mD> z3LF5O>&JkYo9weqS`!}^-~B+zCUqzU1lB(RTF9KU;UL{B_nE6YBgF;|G{mK z)NIF@GOs#JC9V*_f~p7rr#$7d54eF=hyU{6p)^2#%m$o zrCz7<5*8cG52W|+(KD8w=L5WZT}EY`O796{CAWZCy^)^Dl;$f+b0NVt(dSiE-eh|2 zqwnJ=&w2FSrq8Ki7H`j?^mozsAr?oi`y-|MHGThxo*|TW8U2PG3(UzLqjVrSk6UcR zmZ#q<2!0=xk)m{O(r@N2C89`D9TRU8%I5>udpqd5u$E!RWS}Vl4L1|}n7Fe;ubm4a z)*r>tYmjVTbL%(3Zk%rl?_a}a^5Tz4QL+e!yW9bJqppb5jpXzx6cEXdhA{jTG-w#Z zJLFX>;OJ|vFC=EeIe`=0;N&5u#78Da_%l!}ytYg5@ znw@QY9m&QpbUczhL(`YPtf<*La)SOyfaRM1XHkM6CkV-kKs^UD;M+ED<6Q;nmS*^e z*f5?8O75l)gI)q?J(@F=0gtp5Zk$*^OrB!mW+dJn8+UC&4?r!yx`g+qVT0>-WxzC~ zeT-U`KYSrtJ%d_9)%NALlkpZDWr-cXZVyf~2x;ICSEIJ+n5Y5BLTh(pK-Asw{$prU z5b8gue+@?Lm4$^PHcsI`5=d>cpI>532o<7lo$hFWvo7AmGkr7`NDqe1L2+}71j_n@C7F|64C)6^JRqlS7Y6kJ=zB|o`Y~uT_Oj1W(f|hh;|Hvh z>S5)~Y7+hvz*!wmr8~8!iW6-7f!p&raHfV~t5NKHmj{Z88)dQSVT+mhjET#U;w#Mb zu8RssduL8?5)yo1>cf3Z9Q;G!?*0PiZmjA*5`SnEVTRY#YEG6^O}zq5O){+gdl=KU z>9b`_eEq$~4w!r~Zx9pj64co-e``@=cUSn2LRNx4mfcX8U?WZ?yC9eg=MFdpJGfjB1v5_=Tylrd*LOtG|c?OM9l4A zJ)mKRe)m5R3}&zfGUyY4Ug|WeE^BRbvd90&qq-A#5{F@{kaa#4pHJOrPLs{0atHCt zEASGhIRcGl(D?uz!=T;_IuRh;lgeyXOdRw-eXxJOZU}?MAk8?`JAwg&b(+RDaBO=r zvdqwXRgG;BH@}QQ-~ESpr*?=Q|qnyG$E$VLN2#sdO?hk-pift0>;HupNfyHiu$<2;?Q%5p_nR@UM&~`?6dG{0admJ%H5ihd_;)! zy2kkGxSa5W^HS?LOA5&X*=AP=nQ%6=*2JhviGkfH`3~_`VCW(xSH86b_!9&R zS$!(=z(%CT|%9)G`{cm(gswjI5H&Xz~*O zGHRXc9(_x$rAFh3>-Ab{G|pddRA|}ir&Q|&axM8aAzQPSsPA1YC&`l9PDsGcl1c@Q z$GF}_5BB>fsY`cKsjty7~U3#V|t*V$p3bU%aB)u_|#`} z7|RV-DX~t7`CMDtc(ZMxGYOb$~1Fh{9Iw289-Z{M>!%rhb5Vc*c{>*CS3t4!q1maCIX@q z3geLGVpX%!`V2zNu)Sr**cLk!E&KOek&!J-6aaM&v)Q)zrd>0C4N0v%`4C4mkS49R zc%|JZf~FQLjR!o7M1HgTF2QF2TP%e`g*@{PJb;gLwwMcBqvOL$%o|RIQ?-XLBlfO# z68+DOg=&cp{I)o%9V0VlgDWz%#eS#?ZDsD#76Mgd2Hnfl&=y%F=;0m(Y>{@=F{7Az z*&>KE4ISFV%ohJ?D`f5e>Ux*i;*U7JEdyUlL#-4=&k?Kz93S)2w%7pcryVMSnjm&p zWWd$q5JhZi>w+RCL!QqG_|0Remb(J1GSf5>@MeO&64+{*Sf^;B#MWZ^aBwHuSj1vz znh5$BQ?xPQ(#C_Ab!IU}Te+(xZJguAROn0_H!drrjiqVgSmTJLnkJ4l&iC;b9gr$@ zqKTcg)6~N*C}2!ez`0BT(a;!m9zT0qto?#i0hH^n23xdamRA$}G5S201_UmO@6a>d zVvh=F(m>UYu$T%?Gg-^Jm>zhy!R!-9fezFIITwCg>>CMlP^)S*tU)ymIT=K6B8Xnc zGLIyM^|Bl|6bH zDuM&qTgz+^e1a)z0Ig`t*_PHru~yW-jBxFyrcMh`KD_KYo#f-166Z7_RK$HMvYaK! zN~Bou6-Wk^tm|N+iW!dripx!Iq@ru^3~PDG)sfhu3`UibT^hB~M<)IangBmqwMB}L z3w1FAlbWdgu3cH-=XboVXwM023UddgX~F)fiTw2*dEHNwO{r1(mCK9SQubDs8S%dB zb!UqsrHV*WpRnE_AbV?pV}+)ZyA_dTwei&=@5BxtSynVM?2+X;nk@fY>RVVaW{c0X zgsNe#-Z9>iVmJs){(M^jrZFARvL@OAK^&ncbt%yZa3;gP^B4+S6kwZfKfMI}VS;^Aq_2h~eWs)ziDaS#{n)bXe)>3QA#KNv1DGU& zB4;>31VyT-6H-O}A8NxOs(NUUS)wX>X43CNgKQJNoW37UpNCP}?*eT5jq6AI1i2f3 zru6W+z~Kg>%S|Q|Aed}GbUfSJP)cx7-t@L#oO6l@J1C;y%-pg(PG|HUT|5qf0_mZgyowuw7@8kPz)yfwyzk zz6^qyg?OJe(HRCo-6~!TbSW)MaI*DEXS~GHN1uea8Cg&6TJfyJ?q$3N64E0k$+$jz zyt}#+(k@(N%tkS60M$Z%K%e zpe-PXJ!QZ+jzexuwOb36Vf+%Bsgq*sNK!g!mqWV(2!gSbA~3XOyz|bXQrN z5NlD^45fQ6Aps0gDDF06 z>!s%*&i8?mo;#bhgf$^vgk1b(Dln}istNG`Kyc~JU$CNJlfhi|=QP(sUwurm)U^-4 zoQ?vnWN~VYLB|3FpGB6nC&U3o`s&&??KqJT!=%2tMu{vEqAxN74Yce&A-ZqkqqExM z6H=JpdS*qm3#n#28F!_caY!}eQmPr3QqA}_NHwG0-Z0(OLnovjIw3b4$b~?Ll^%Lz zLG&$CeHQqXC!FUUqUKVPSXp5!q;c+bj1QAB-+uE;(0H24Ba3v>fX^n_t09&$Y5p9x zMi-TgWoXSH?!1P2;d+*;$dEvNn73k-aV>ST!CTmoK(>Y>lyl_Ut`FBs;oP3s%KIxn zu1CFn*WeR?3<9R?IIEkkm(DYL!6{>DZxtyc?p*3*I z!OIW1-OtO{t5}XT#=4h`D&7B;s{g(qJC^EhkBA#C47OKUGo|J8t@G?Gj{?)N_OY`(C-HupE0yP zE+p=Nj1vZ;N&byXzXaIge9aw2e0h*}-jI}Awv5E)_vRgtb{5N?;!OTRVmt~P1c$+9 ztcr9q@KHwKOQrI`J89!Qi7>GlNXaBSBq|~CT%?o9TVEPs#K+_*#I9n{{N)AZPf406 ziJn_`fD=N>A~v|zq9Lh}_HXAko3;ES#9sgVqH46$%F+Zx&kY9*OSYSLoshVo-RHmi zu42dE_1N*&nuK4Ldnz^1r#kA_-j(Dc=#TpXHR65L*%qr%o(nim)$KlcE_@kwS1W1T z?)0%zsqDPoI-avz#x})v3Rr1X258iKW+|OTiS_NfL5x;7YXxY9kEUEe8^l2Pz~E>c zPtg=X69MP)Qw`RoVfBN_hphoF*Fx+CtaM(*SmQbb$2zBv(l9u;drSCh!&MkK^PS!7 zgw_W@(Aok^*8Br^7Hcw;izVc!Ex24Ttr()9)N--)x)SQ|y1DSgd!IkPjFZ*AjgTVb zN3V3x4h>VZTH{-+$osU?cQI4HpzIclhvem7>V2yiHlgp49<7mUUJ(9KF)7 zY|xY6aulV^&Zmj>VXLQafc6)XnOlnV4S+*Z^IMZOM%tcC$N4Pq&G-45fYxExI#bVK z!zprXqY#m~z57!2?n|$CDW)c%6;;{uv7<4)JDBFVlF1?7Wlxr`Kuws?ShDchem@!= zM{V|YttEM$ZH5Y!zg^>ggO#4ttxn$uKjEh#Z)Ix6z%or#Ep?m6G;&LRDO(Y*{l*{j zx!Q^{7W>L5d2iTO#LvA6^D)klf$FxUEyd+5*^agp_h)HQTl%Uj%J(xh3xj4=&O)2% z*Ao7wn^0-1ignZ_xR#ES^hsBQ34Z*T@vp$Q>Wx|p6e_!H3Q{q#%< z@J-`S1X#}SiN%pFFQQLeTmV;u$d7rF0|dX(RX_0({HN3zQa7z}o2d-{?6$ z$ewtxvBE9(zohRg1MH256H!W8Y=^{D(544rJ7@>B(LqS;ha(6~PQtn4p^H@V2{8X+ zFG`ngz8ie6I^fs%e7ePT&;caE`DbVbQc_Bon~G!4u@E$~96TgL(pbZfu{<$^R6X2A zU1~ZS^Y;#N=U$Np+`T!)gE+P{jAl7+i0z{;HPM!b(H14~kbVR()=o9!P{L{jI%flX zML-u{v&t)82^J1T@r_FZ+J-D7W3&qB3d8vcls^9kol<097*gnGf6IONZX7hUa z>O%SI828n9FzgS>S5tE4GLDplTUS>6XFUS)5k zT>0s(i0qVBeoR%(b+g(ziDG`X}UgBR6&i`7TKZmR?(VIzLj$WMODdd2`S(@iF&0y#< z%j)1VJ5~PooxwxmO5}l!c71J~Ra-mtcvpgR4wR{fRCa_PfTXJ(+v%matU)g4?9I5{ zb{V|UC6er#^N@t4hGGcpQ5u)GYwPHcfM{OC+qJGZB+AjU$kfOfsbp6&yYC^9AI^fi zQvF`#^}CL9yr^B}(#`kyWLfJ0K83)kX2?@n9TO5a0elFikHOUpo(=G*cI`t+{okbK zKR=6_p->>xrXwkb#DUN*%uhFiB1i<4XxYQe?gufhPgtyEj&++L$e)@Xv)WbJhopC$ zI7v1VbEcZAq;2pc5O+g!EC>1e)!ra08Re_6=Wm49g_e>%hCI*6U{2tEvcCj+Pr$%| z2h2{yH*f&i{qYU^Z1zD)*SsdC;?M_Vtog{;fvq zICTXIahJAx?FI%M+-10-k1Fv~N7HO>;@jRe4j+1UJmto!2aP~d%2~>dHIEtN^c^bO zd2c}T0Y&nG=HmU_<@mb12ZK(N{rj`OU%>d*l6sqBd!mXYcODxm(B)ZdAt>987}1)Y zM1!2(bFHN%(zf%mXr^DzV9E>zUIkEyL59uweALSQ?0fb=#`c{W!&VUvap zbTX6-FR2Jp`;}!>H@tbpjpk-ck3Mb*?t<49S?-vk6;S+sL@nAywP)dgF`FeUhvRhS;|a%C5sS8%K`SmR$xYfMtY`iQ(94K~Gys}5V@ zyXnC-PM;f09U*tU$XyqQBn-ciuqKkMwcOz1%RUNU_VMr~+=$E9RP5Y&pp&;!S-As2 z)n5bu^r&QN4kS#Kz?cf~ip60!Bvho>iC8KXir8@o*G7Ows?e8Ehh^8`YP=7W*0fqu zR$JB~g<;vX`Yaun7BbF#D)unKTDNxZA8v?hatd*FXKTiK8QTtj=nN$N86~UM^t9yJ zs01x6I=4Nh&!bA0t>Za)(JvcRISsybsN@dtfkG_kZn~s_4eB`iQK_pztmen`judsC za12>5Wbt!4>0aF6=isiI>Dk%jX5b_Ms|Z*?V09g;W*p6jdCy1mS!;6B($V~>G_}gK zCFrcN?Ch6NpW(Ek{Yq|h&eoC9=SM7Qaxk>F)_a_fiQFR<)qWIkPjb$(BFdUh`EJ01 zuPpnkGINdRmni5By*gSEMZEXUsCeK#phm};V$bO z>s4LFU^!C=io*x|Nq{g|ACRBGNDc;ZN11#L<0bSthMx5%-}!)$QP4XfFHXmn(!wLg z3y8Fpy^GfF$lqa~Rd}*UprCYpb$wc?`Eg4M4DYR|j+BZ!9$29Bs`4+Hts0$|W5ei#~w;6*GDsHb;*@A^!=j&zFstxS&F2t?R+XJLX`sS~`Fs^?MO@c?_m>Uxck zufq3@OM$n(#w?tEoilVaRJdVh??&uTZ2THoWI)#Lo4wed%>Y2N*@TIV>E&}8s;D}A zZ)5{{?!H-{XM$pXWSQpE2#!R>C&~g6dc7e$d6_C;6s5)D(D)ul0rGqw*9-A`xC_Xk zfVciy${?fJs^#_pHo)ajzxTU`PT7t3z=)Mbm$#MaPX=W`XwanJ)1a&isyLyrRGAc! zH}I^d5__6#?F|n|=ySd>c#ni^R+t=m8R5w=G6*vOeK71oO=Z;qGA>87p}z{Fe!Bn; zO2mmfH`u*LK$gRy#htm%bStLJZ!Tb4rm^mPzp2gwZ+4dEsxag*;UN0Po0hcd{0mT{ zUQTaZ^aqmm9&xwE%DA-ZblyQnV92Km2WYOqGH+z|`)DvV3fiu1PG z*`VO{Y0IkzJuQ(XF7Cs;L4};Iw^Tf8so)K#50~o&mnQs)9w_$FOtmM^)D%wzDnGrT z8H2j3&H5lD!G)_IXWfu@_t+#Ugap*G1j7xD+G3i7g zrCJ#$Byjqs*ILtNKbl1XT(1-10RB1&Li5GKR-un?XX$qv<%w-CX>lj{$ZM% z70F~eNdK~ndg;qaGScWp42)=xg^XOUR&8RBIgpnPW|)kEe}9)Xz;`SqYI+3Gp*Y;51rZQq3U`GbK>SblJGL@gZ2ED9AFDtp1yK-g--o)2k4pDisJe6N6 zGnXTCIcFZmnRh|vdy#n$2J~%%!|yQO2+)5d_ySwVJ!@b+=I$OmdT~zmcZj7n6yoR1 zwdS-cZbKv)N#z%;MH?b%E`z>hs5KF8O_YmSiDGsH22`k+6dADqu+cQqy;Pi)jJOz} zhCo+6?w;(P%HJky>y93G=Q{dv=5fgU1jauR<;V#cF$k~^Fjw&?M>WWZYQSEEqf$kP z<>VO=h9Pr^c4r_Mtk_{34rjq24G=$`7VtEPfW#uqj11u6 zT#kJG8886(p6_y8yCu7&@>AAfT)SahyYaXV<;-6pP=I<{|^8F|NrEDcYKvq^7nb3=k}81CgI*Blmvuu%}pTmhu(}d zVS&(lZ?Ys@KoF$pA~qBYxB?c$uB<4EbVa3upt$aek_hT5vKFu`C?WaH%qh1dBBG!7 zpO^idJo9{?Idj^~oH^yX%$AkT7)wbrZ9i93m^bT;Wi{u(Pv391W(^9-XuRnb8 zMXh+P&l@n7_R^RJRGJ0;1Xt8xwydkUn3*W%I}K`N<-k}hDlPxktZMwd5E(uwuc)xh zc*en89@`|t5iE2vmpe=29F2g(-trh$C6gnWm}lq3n7R#`uN9~9y19LBf7rer>*fwX zjoiFOUY_~WbC7vB)+mbu?;&s<0#!;&_9gqnHAz}BN=xR_EIjja%$)eEnLR#_KirZs zdoZ(yXKuwa55&wzfe!Zo7T$}&=V~$A(ms$m2(PZ-y)}Wq1?R7i{A*cEVNqk^^f~?E zGAhie#>9zZ;^f69aWNdp8Z(7Cc^q=2r_cHsU1@XX8x8CeEF&xPI%?*jht|1i@k zgSqDFh9&qW3>`EE>wCATuc~E{sAW-9Ej3ZgTCzl5+eAnwpvLXsSr1b1Nsyb{QZ(xQ zkSFYVSt|~1fj57SsL_r{#{^%1%I3O6)1C~y7gaS@9qLvS3c16|eEt-h&sL!Ej*s-` zIfK^}pDe1sMICwF{jRWcgI1ii59k^F&|E`ydr>DSF(=E0LvIUN&AYJ6Z3VUAu?*5_ zsL>gad>^DcfPNdE#n8@w06iX3TS)zZ?;1$+;agpJKLm6ic&0*SZh+@Dc%KZZF(d=9 zRe)*0dkmhR0CpVc06eR~a|)z0Kw}dJLo&#=+K{zYr#hM=pm?(r0FFt9J=h=G;mS(o zpE|=QTdQ}`nYuaB2$|R6z_~b(1%RHJ(zYH-OyQUZnT7xmuD3zi^Z_O5=Db^xw=32ofdkK#3#XpDnmc}*`5^Kv!D`gvz^w@2FlGff zFcScK(K8n2%DszQtIZsROb=l#=D%`D|C7o2^O65`;Ll9Kxqb3L3K8Vyo?<0^1?+RceAdj=!3A~z>cYo=!V^{W0qDWU;ECGf1e(trHWOzj zvjzZ})$~l~;6a|O4E|vyerOHlW%@kapF#T(s}ESX!K+nuFgA3^ub5N}E*7u{{$!?jvK$;sLX0L+9|tO6jx_=u&LU*t(ANNk#$_gX zviuzQ7yxrVaOj>aJBRimGzFmW+rL`*tTfL1Fv`ozOz`mO(q`U;ne!lXvKBl9W0}oo zTALZh54EvFlQ=LMfRZj{=$kulRKB@60I@!KAfyd2Y)(tWCs)t6u`M`A;76)~53}V^ z&wJ75I+YjeD10vpTJQ{NY=A7l8~QlJGS7HO8N8vxr3LBXzd{xC-~cee;7s&vsj2?p zKr%>;n4T+L?GC?eVa0rO!hZw)-~?${CG((|%FaqTwchYq6Hnp79N0z;`-0in*vTwQ zwreXOcUz-;_Y;uZ;u$F;&LcuEuuz7h0JXI)q#mOEO(Ee}rH7AzyNhZTc9JJ_*yeAP zL7n0Wy%l1*?Y%<3i~7YA+7R;U{zB?lPw1%-pD;Y3)ulCKdSdEp(LGU0j-I!xF`K-a z)s*cT2ifl-zi;tbD*OEw8u$D7jB2K8ogCv73k-4yB%pPmXF#2JANsW$JcmKTd21@9 zJV?7CS%cG|a(M%|hFU_qLOvXO#@}8m*@G_C&flK35Oc1ZTfHUp_fReFGAyB`T!m_J zQ?i&h;*f8&dH955F(<=&=}nofDFr8XLUi> zu mr%25kL__4@f|EdSNjW%^m~J1wDMkTH%Of$3BPTkZ!Cq~p@u=i7$D+L$i}tZ& zQAy*f9FulYV}j$;O^n4bF)o)F6C-9!tnp*QB4c8S8xvdHnAl^+r1}4&G1*;tOd?~^ zNsL9OxUpCZJ-CdXj&MD^oG;~k`uB#56^Gl z`6wiuPBuaHUIn@*q@%)4S_8dX<1vcfJI~GgC^57yl&$wJ8Vs%D6J}y)m3bP+&=VMx zB4)J2(2|e}O!XNB7)H@Bc%6Z} z$7A5`^11w(-bZ|{qD3^J`Yb7Ke~M7EMDr!g_Be@$KArIe%x#{`rZo2cREMKTvqQ!C zsC#*i1?3o&tzj|-OEjkf5}M+3`#h^stZBShT%2OD-aw`c{z8~*_1TKrf*@STh`J^C z68xDrJ>pB?b#rhHPBg1y6CL+VuHOEpa=BzqG_6?GhY?8Nz>hG)Iw+oMiMGbWf<&_f zLxBI4%cv#MJOn^#W{Q)v)aT@C+G2C8$5wVf{UV>H@L9Ni)i!55k)SEC^B(<647RQKPgmt zBvwKUZ!(IXgF46U%G4^9O5d)WUZQy#2i$r2D$Om|e5DGNP75bp&!RM%PyfPnZi#5L zFz4bwO{2MgF*B7$FN)G=Ck6iq2-Q6wr_sW|PNGJiLq6^V&3CKP=oRrA9Up*=6`GCD zD)bp6CS3oGj?(9pe4TkA&jYB>IS!iJuu^RxjfRA4pB<1sg0z(JIc^riwJGV6Cv+(I zJg%H4h;V90C_9|BMXa2ub3CEv%zwZ$;1Z$LD)UinI<0DP?A#GM;$awhLlIf$zD!-> zF-PO17YM(iWZMYtj&L8<0~19EiqE*2oX3J@dIhf0s6SJ-YQ;r+an?;Djr5phI9`Rg zDCO&TD|;56%SHSh$O-u!6nEnS5LeN-0f#ZD2s`!OV2z}Hqe%Tk57}~aMSS^Yh=uDz zsmUz38^=+tS^aL&e~*<`6Wz2zR=@PN?9#=mh1ZJ~;l-{3Rx=b};s%XO0 z$U4CuStq2iaW~1jTUZ51SP{KWIHmKb4WDwK1npCKUm6RS|89s1HFqdgHZ#(AdroL5 z@Z*Z@4M-njO(65nkj4UiD?A^B{m($XOecl0c2;d2~LPh??j8|d?B=;F29^zEeRJ_|);(6^yX zW@~weRA2bqsY2|e+Jr~4`mFxU+(&#?@GyWT4~hw&{PyLo-N*me!jz~Qc$-?r9gbMuMKZgwvh`?!2g zv70SP)V_(Vxab=cwTI6tb~6h(F)HQLk=;xr&X;BNS{78uZ4WiCynT20! z^U1+(PDCNs!5>d}%nq&C5j5<`T*{m|%7^!av<7p?6>Q|^Hw%VsE2;J_4rj8^YqKE$0hp1_~ZRy{2BGI{PB8N zqUv#_GOT~b*jgiPxxD^}pVvT!?f3RaB^mQv0g>6+(lJkh;N~Fdy)FfVG9^2;2(+ zY#e->1?dBL&w!`P;8XtNK;Hvc1B0LK{G0Lp=M6ybfhX?SJPY4;0ngj;4JSJjuzN7U zdkf$>3)orsegd9dAq|7{ve=VZ;DO#K3PDUr{ZcdrmxwgirJ5~aW(}{~LGdGxaV&C! zGL;S>%-b-=@%tP_4R!1gBm;wr+F&#)Lm!{d=f}00uc)Jw7GqYP|2Qqisx=wH4^>jl zgYu-tmg%Id_1^7jW0v4F5H0djQ6!z8Tcz^zo|@2}v=loJnNn#bX|6V+YX*@@(R{`U zQ7=5s#+77p(KQbI5M6f%F*`Gpf@9$oqBeZ>*?jgH=OIQ+aZ*>oifbRPO5;ns-96h7SuBdvS-q{td}qF zo@t;qAXCjpQ2tMNG*-R?mktNy(!rnN&bo^8+z31+&tijWi}44*R&+CRwfz~-;;M1H zHwr^d;){h;v;75X>wT|F-F*Zv$z<`5b*O0<<<1eb|3Yj;-Wr)*{M%2z0P`TMxSIo? z|AL(=8=mA#qJ}3?!;@sglZ5{N!W+I43gA(Es`(@eyojX-IPd@fB^!`-`CPMp0I9i7 z)EV<(z`%Vgx3=IZy#KQ#dcrs)R=I~Ft6W$z{~elLov#p6d?|j%T3<@Q34hXjR`{Fl zgEdb|hR?p#X)v93Dd0WW8HPAG5w|RsK}XnlH%FZx)2BB;vd=|ph7c=dC3}9ZsNfzr z7d-HBpF5|cnAC7jeKxqty^yeRxQ5UL-T<7)0vkVy6AN&KeJ$m6y*o%@W!HJ#qxE~&1Gd9`Q) z_!#;c9SP3j*aAFN1zn)sj1ZGqk0u+ziCB;7@?>UbsFh6> z`AP|H`T)-)GGl(|5_&|p6`chS%{Po*mCafb66;P2ZFrU@Bpc;^o5rdaO@)y;9`5%B zp16_)O-7H-3pC#Zr&8L?O=^>y2)*zn@Y%)SDHS>{ROom_g-TheJ8D!@b1H`-bmrRe zI*5wkc`u|Dkns9M0*n3EN8_nkln&s(+clmdI$*HpFRcS9LJ#Ui=|PkZu&Q)Gi|9b3 zomJ|9A$6ceoDNuiLkHye4-#XKIx&IkfL=)ltWpPB+dQ&TuarC&tpk>b4*UQ*5Ga~u z1toYzR`Ax)Zh1C0yDBBvyfan_cpc20@k-F;%|BHMqIKXwNZD2BKwpigcpbPy<0;w? z+zQoM4LZPg*g|@GPH3*Ppr)|19NcSy?5j1daTr%%G>@mSLjcc!XBT(}FUq>Z`zCl_ z2npwjCvi6eQa^ZK4$r@0BQ<_A>05Yi$0~{F7EcyKdW?EuOzEXV4ZYPqO`I=y%~2Re z4wjLT*Tj1XmX>Z!?7Mr-_Q>!)6B}S&vmv}2S>-^tYlmE^gyN2|iLRaEBCm<}Wuo`6 z9w+tinrA6W!(Mm*#?iw4vzj~8JQR3%X(O$qF$QyfddI6r;Z|>fhLC@u95N`zNSY=$ zulWERQrfAX9GdvWtcy*dLysc;(FM&SUF*D3TEd29ZVoxhW+%qx%iS=4hwbci+sCS zoXT-sYL4s7*}Gz<7pZdSse}UIA$(|9c-yY{vE{~h25kc`3i zSZm39RXBy$&{XUZpY(Jb-yorf_mm4K6Rx=%?W2AuGAKuT$+EuPsxWp4^RF4Sr zeE;IFILC! zNSi_Da4+=XIz85A5I+FK7kcd6w?L~j_ceAFAjf}%H7tUJT{nsG(^J%0yrGOS2u9gY zu^4S96%TFKTj8x;asBSaP-eK@p7>!S#R=VCf)i(W`<}>{@vcQjS%OtlC^#^RE7VYy4cEtD+ybUNPLB$j74f&EDSn^uz89omcAnP`$9_<9g92oOZ3GYyc zHVNM!xTpf$5fv(W(on`@5$AliEGA}bSr0$)DfnNw9fh|fgtOUG78@kqDQ~2E>9e}F196$C7bi-8b-8nCTK#Gj|+TbRkSRlY-m z|I|Kz<#Ps$KX@8{=7DpvkyjdKu=6P|gvb-u!#!g$P! zzmaLHafgqcf?MWz^KYic&sA`|UlsB4-SB)Dp1A!~u5lfR8gFu4JU-^GiQ?lvkrmsN zO5?_jmg3_Vl#kR(rhW3Mmvd$`xU$AjK z9FCGcD;Jx)UbAQUue;vAGd1UBqe^xT=e!d!67D*^>CU!eELZ3;0< z|0`}inIyd9>lbC5Ub&%sK6@DB-sZyE=n6@ClxcK79Ag@z;{)AjyO;&Tj=%qkw+lQ` zI;cncMV-z7ugBj22BS_L3s6h2f0hZ&#&an=h`dEv2Pua;bRhiD;mDaDUKphAcZXiC z{OY~ioEE3sjY(nL<^a^({iF+Svs;``dYO99O+P9Ny%xwx z&!OMub&vPkO@!aZXWTu8%C_&%3FSEHo+Mfq{#0!_q}w3j_3+M&@1x_dNhh&No1os% zu5jHWGNLwOCT+ASZpop?LWuzx_&-pBhYe!Auq3*9(v4!@fZv~TBwmrY=Klq^B!@=X zesn)RB+MuWWo#GPRj7YIn|V$6&92ew1a8tmY_3}|F-v! z^jF>Ay65uSGK~3kf8(CZWsHBlE2u}D3}ZeCsiiN<;lkV?t9@0nlnuwWWC7;Wi~P{7 zrABsXR8cwN9xy}&jhOL0-r@zH-Qkk5qh%UJfyzQ<;W<)vPL7oQTlJ2qZ`vh9t9oC3 z^_A5-{dd$GV_2MPV3FXW!L_mGKw;OqXeefk(bBIxI(?G`RC|mRkDquT2%;->VGl52Hift z%)Tkgyv)82g=F9B^jp2ll3$c{+5HZ>Px51ZDfh|qbNMWg7v+Dd=CQ=^NZvBnXzjn{i2;NzP@%#8y$o$Kd$rN?8T~hXy z-D#XL5U=|G|DGRzi-Pg}!*~b66=Lzf#J`F0yZQJ_d^81p9%^uI7xC-+kMO2~CV%k6 zKN{grbXH7*m9=Jx#*($7L>Q}V3;%gipMd;T{v)9}s>&zD{@BUL{ur-d7Lt4|P-=J_ zb1OH;7Ws{hg+02K8FI~+AJ+qy7lZqA{y;1gti!|4KN3#`uet&madAxWdMB5G|2{ui zI)EfI{yXlniAk)iTN3MAcunEEC$<@L&Ufjy?a|}k-P(3Q*JJQ~Y^!0{6-_LfF>EUS z9al7S!mJr%N6#z@54%RvR9_L>qM|Yv3ENwRhr?!7o}9=I29< z=UcU*-#{^0n@q+I*5)hysj$#p26#sYe|K{mV7O6@huwHfcR$`2bMpfdT)*NkR&fm+ z66%9_&~|VhUd%WjW97Z1a@lD0fSXU*bo1jnqHY&dGFz#>t`nGzmB8PFM4eU?t}i)N zM^POWHAGP}6m^fHa4+sD4XFg!~nMiN%6 zD7-;EkEn+f-#-+#MNyyX{06s;ol$%j)K|M9F}$Vh!&}KLTVahA)m~9O6*WXr;}o?N zsOcnSm7?(eH!p3g!Vaq(rRwYFf=XasaEIR#Uu}!b(Ge(cpg{Gp$h@O0d{k|0p28kb zd`~LuRfX+U)X(tubE49$k}p$H%@uW>Ro0+LV99K$pc2^AR{Gs5_Ptd~-YTdB_KEs> zMqw_{^a?9d)fCl8QG;zV$8?)4?G~VJuu-4dq?Bg_Ujo~!sH2MdQc*uEc^P)uBHSRt z7A3Iyg7UNLfNBb<0lNeK&aor5SzvxvDkvMf9c*}x1F7YT+USsycROUi9Ct{`HmA&+ z?UZ~C6xBgdeHArTQPTxgsNsKWjNAENUGnR4MLnUYZHju!B`s>7i(8b7ec+O|Ro5-Q zhTQUNQ$>wZ)J(U`ahsdB$ixNl<=v7^t0YvOG8U0xo9pNWMOb8ZD^3+QS~% zwj8gt!E3#;9qYZ);`b};6GgR7kYC3Lst!ArAbUJHQGP8@R4+vhR@68}O;gljMcuEc z?Le(dL`qMRSgj;kmu!XoqNt`mNgeY^zJ|#X>!7HiikdE{1lBW!u@)&vO%RlgrKU>0 zj;T`KU`4H0Uw>3o&1#a$5mW{%1gdK_#70)*c{5nCz%tlkMXeN60&~C|_Z3lTipo}0 z2Sp7PR5F|8m-c&;U)Jw$e(6ij_@(!`sIa;L>8aZ)YJ5O>RzE)8>g#!h zC8Wt&C`(ayrAfZEirSwhy-I4j?B_P=(whxSmp=CTbY+){T9Ynox=B%|(q*4c%8)%7 zR@7oe4XZAxdla=9sHdwVbx~otHRWj4&6F)~DX0u)&yswtYRkMM6}6_e{Q7~SymcfM zP}J3my0NaL66;ASUs3nhlXZEko^0g>^>s+L{5mOH)^v`d7G}#jugO+5s4uBOirS*6 zy^2~Ml$1Y5QneM;NKplf>Z_>niYivrLPb5KsFxJADkP~riaHyTZOhMg`HGXg`)a2R*_6&J~9bxd}BEiPZt#UzsAxQ9#YgNjiptbR(a1i=C%)#n@DT{ zP}es>YG)HE`A8EvJ2z~~siE5SP30W$Fi`U$_0YB}Y;RN9^5kZ6KCj=5ONN-Rne2_* zn#mSzY$h%K1I5?4IhPF4Tyx%z3^oy{Ud{2p*{}@$E+pz9MLnUY=M?q2qE0BPcMDE| z7PsJIS&Kc}Lgh6jRjR1}DC!4Al_^SZDXB4vnyILRimKL1Qbz=3WB+L-``}_L*=u!M z!{64_bCz9gIJQ@NrY)x`ERiwV*jDPsfwnUC zDF^D4wn!zklhgo3O%Rlg?Eq?7JH*P=*VOi$N??Q9^D+0c;`TBkTdJr=9pr4krK4=` z$xd7z#D9u1I!nr{sOpO9r>GHv8fcxj^T_j&OLHXJIExstnR^U>SxD#aJ~vd>PsEw=*hAFY7y)MfeqEh_L6;H2UhDS<(#$o zVzrUNqAB>Fmc%l8^Y@j6EP_>hLqJ;e#*2Xlvrv`+@HVzL{`U?YESGo|3#5Q~8U9D6 zlh{&$q_TQ!HZwr-V?uHPyPD3Zlh_8ulf&*HWS>Gp?0$ALRrThAD zIUQLOwx79x#}r6U)=~+o^szCa%-U3+|g?s@` zPhu+od8QB6r#JhGmwfM9Pp z`pVMh0AfIWvII|mww$%l9tR3n(fNe_gALGb1|D4ZBT@j!P|XT*{vvV}v-NDghW`;% zTnix2VD=L2MI^ByePynf@E$$nnxc>mbmcUO-7JvdYzuo_!~VgQ1R}+3E8D0of?W5g zT-(@(#Di-G&a;gjr_$Gp(mS%(Ss5XN6D9JNKnfJHM;3t zbsclzT7u^~##-yxKffrRQ-GiqEEbYq0;CHe$qM<3jUfcrA3WD-HlL9D6C=pIRMxa8 zWUa1&MvW2u(}#V_UM8eNas=5($j~Tc3nAzYB(j5$=}|my60)&x1ldjP!c`70{adzQ z$8rC+821zGJN9oKNAOdTYcxBHlO5l?I-0FXn&(t z3=up{wVv98ge*{UdTJ}G9M5R&Z7Th6KiSf0+6l^aR3N>zBJC6* z$NI@WoUWZC@^nqNB2TUUQqBS`laPA-Wh)kGjS1-r$OK5mfHbpWeIA6j zNBg6kTeLRB^M>NNMe9JwVTCN#x)M?nh4digoI-Bb1`uK$z>%5So!W3h(xQ-2gtSt~ zQf(q3JqJiR_h~Z;8KIB|w0VTwsF3B_B0_Fga{jK}LC6|~tkNE!mTm%M{{U?1!`f;> z&In|k_OSLSAr}X54fn7|wWmmqb0Ft=R(n)iPe=`cJgGgdZ6+j7@vPN$5YkQ|PicDy z={-=E^|W@7ka3FVS?zs7W+~)(?IS{#MIj~DH89?50ojBpAyff0x8xcazP>M zv@fmb>yid>q$8Anns{mtk~}*E(m)|QwI2xSu8_C2GD4;(WVdG6D3(yjKFveO(+WAL zRU>4xLJn)0guJ7WBU)WTjz=N&2`N*^Q7xa4gX zQAn|VCn3cOnXTVT$lnw)SHGW-2NiOYzMOjXd4cp{i}VNWs8L%0**Oe(Zq`@X(QADx zkXQAa^%n`Lh(cZ>Byl)Lw(Ga(8wsf{kXh_jeJhn76v!@pxqg(8CW>c;{vqXRr{p}T ze`CkUvxnk&Qva5C1}kK}Za7fRRDpb`zoa_}nWf5lMX%;SIg3=TSM}-+lyk>$+0wW5 zJVI6p|TyBBj z9vVjy1)`0RNQyv)srsY|BvDBY3FM;UX&{gUC8x1KMyghH00dVqxa)%mV_gJ-d$tl8 z=s;b?ySB3HXm&SF*P zK1M^1C^>zN9)#fTDMt#8n@P?dh4eF)5D)IyA~MGqU@Rx(tl}A9tRdtj!E?JY(0HB@ z+-2pt1{&K5=_`2d1!N~7#Zky1LJD#sxk?FH7{&7oA>*SEr<+y^c@d;3A+-h1vw(Ca z#2nl)07}O+46#14qiT?lVje*`Po=veia+;`ysUirGH_K{<1joMXmNLhevV zsWE|&hgIn(jHx8&ImL6rC?aI5K+-HHjW8h}DCBeFMmoJbEs$H;7sf)8d|pXDW!y&n zlQ>FB{>r$Ac#GQG3P1pYS zSDg7IhGR~|5L2lb5*iU{5K|JQ#-OHZ%!DXajUr;Y)m~L?QEh2&-Rh`xGPNkBty0>$ zh88sx)oM!>#ihUX{jSeiXYb>1{hrtJ+~<$H-mm@MYu;r(>dsAU zI+$3soZH#-HKvYp51UcO-03{XX0kEy&L7x3VN65kuWVj4rjhdxHt!nK*m<4J7E_)i zr?&)|Z%mk`&Jt`+8k6iS%jU8%Db83n#dEbub=G84#h5f_Jex+wv~)IM)7qHU&a@KL z!*iq=luNz0oin2Z^`A#zc0042IVJ3?sa>3-*v!k-rmJ%_r}MH&r-w72(^&y?#M#d| zhhwcZvF>;Nz-EUrxz3YpzBgv5^E8{^jTz-U!^S;Gn+Ke~v57Heg7bGa4UC!S{DV!p zF^@Peu*orIit|sd#fU+=7So+qxE51kqFl3_j*|A)_`9*5MpLBlD=7GVwgr}XS*vuTP zd&^npA0ajU(-~RH-dnCXisnFDek|I#R66S}!R z<1n>{=+@}&+8$EEdtC=ZTBEP)Fvn^>M7Ksi*O5|mt;~RFB?h=oa+rQFB}A??wAM|} zH&v`$*C|eU%n+UOV3#|> z)R@0q?$Xn6l#j3x?m%fuhn^*qk@rpRwruEWwK8R;p>d3!ZmU=oC=8u(NGH}^(cO#9 z%N8?|&A*XOO=;$`ISf-rnuVP5k)&Xl7dgxmFpZ>nhs}9QI#<}--%OXMqWe0V@y0~E zJ!Qz8FtK9X<=DL1RHq!{uFj@dvtXFJ%Funv2Gc$8_dTyn8P=it2da6!$ZwoH<)fWn0%X|wSvD7 zH{%%0T4Or62bH1TLhlot7|yF$?h)LoHDU5aXP6eobaPMPSfxe?QDp?h%5l$UlWt6J zm~3PE!Fcc1W;DzI6Q%$rAEvjM0yE2E7O+`jF^k!(wwNVszP6Zuu=&|yUSZ=N88ol6 zX<#uc*yLEuJ8WiH%m-{1Tg*B(A6U!=HeXuIW;SOmW*ZyVsG!--rl!Sw$)<(He9h)Q zi`mO&s>OWEW|_qtX0z2|j;Zok%t~^nZ6ERwwBkrHdQtpw`RFr>hJm=lD%F+E!Pq{UCG=(|uzM~w4dCn51E{Azl zZoNC2p5mQ%*W)nzU^;rPxRcl%hmo@#cS<>dGmcCvX;L}NuO`+tnDcTgVvM{(NYjGz zx_gW+&vkb?hshYD!$f%6vB@!}m?x9XAdBh9xj!&QJrQdq9G-j1(U>p==2_vEhR5(R z`bntQ)1A#Cm>(U*J%3}f0;aks>6y!U?SYZ^n4bCN?6Zq#&zf@f>vF7TEt?}_w5j6x zu$+C)U(NFooAV}2bSZQ_r)NO>1Ksc($?WX-p%}=WIqA z)7Z0}%~WIV^6X^ulrc>`UzW2!nQi9Tt!iq*H1~YPX1y_Yd%j__%b1p)J>~4P?sU&S zHpfkvwx0cLE*aC_^BtSWv1Xk29As0~m=2!9Y#JNW$@4v%_Qu@fIm)JwFVdo~-h8E{}$3CEoXBbC?2{e6d#=3iGH5^P?w^!z>=B%j5CpahUf^ zm@?iX4nyzrtsBR)CK!4nkIa4ESnmsL=*?*|t3<5#OQfxHub#kajXp{R(>MoDvy^C}KA&wH2m5{Ib_lOK`nrGN2VO2m)X$D*0n&8CGhsonsa{>HTM zR;obhXcw>bx?&E!o69sKaD>)7i8%rlYq5o8HEB@%Caf+L&(M0c>U%)5|-G%>rZY^^Rk+ z{6SsjzTN^h8%%iydS|ow+Qb^@eU!}!6D!v{kIi2u)-dn$Y+@$px{vTKW7BYgPG_`t zRR!wLnJ}%ygWiwXAPh2GEEESaElFZAwU^PVw9-aTx-G-jsv zFq6MDw}dJ-F@4UKWl-NNQkm>Isq-fdj3r=@vrBCl(C1JU;P0i3?((UjM_6LnsGm``DPixM!$ zET#pUzbvLTn~DzyO?x(tEv5sT4i<9{o52>-jmrM z3{GbQ!tC+2^}Q8Mqv+=*Ok3Y7uElOsi%j2#obm~nBfd=ECN`H4Yntfb+Y(JHCy@p6 zZmNLN>FC=U{h~ZhT?QnpJ^tPA-T%OzloqI>$K~8y;G57e6bC^lS^z>a|GuxP6 zzCYP4G^U^LI-8eZx=7=Up_JdZgmJO?2*&Hr^_7dESYN$ zwPn*|k}_*Wp08aDwc(ISx()Mu9XQNrm>n`!S2h!2_V`Bodd1K&ErRJSM*I43EuMur zBEt-bp(pZhnUqKS?&B~Y!{kddD2DF-c1+Tg@*}#0%YDTwQSMD1(MNE(FTi24VOoh-edR0B+@{wf`sl1= zBiqNM^S&>RO`*lqVzbC%>auy?r2M|GKIi_aNqG&-0hk&7wZ4XI&Kk4Mm%zp~IcO3q z(ebJ}S-0wXUlTU*7Soh-Z!uZtzTVfY65r!b*2jGV%mA2YrMa8U1dB;y^QgtNVDp^C zv}ChtvaZ(#Un@52Ev7a7+_BhZF>U#HeLYzpuZ_MTm1rJ*2<9WP)i;vs{;M?CCsUZM zzPw7*=kJ`NkNYX#MmFijT!86q%r%&NV`}-8d2)(Ack05tZ!ufAj~%p_PdUt0i}`}X zRGk_$yEsgm#eBnI23X8KZl6h0)ylw{Vs-uBRiZoknJ}A*)$@PP<_Tk3`p5pUcoiU62sj)Qn{v^%isnqiS z^tXf^V8OpZhSL*?mh%s2jDVyQ>bzfDrTcaQ%MHj|Cn zFAe>E#5`k8!z?%ES28lM&!*|tI4=#=^nl6hvi}n2PTvn{BPx@jZxXdBU74Q_SYnl9 zbH$h|{wiz^m~{T_uUk2yS}Fcwl}s0L&7V-2T7!O1K()qS{uB<=+pWV?jZEQm65KkS zs*!1I{Kiy^?9FDdDPfJsTsH0^ZE8ghV^ba`QPhi^#`UUc!qkhLUYT08QIT%3dXcj$ z(;3&?glQ)YmHFNx-NUmY=Wsd`Oqi_5xol=y%se)8jp-2iLS-5cmm1SK@=eZtoymPr zWMUOEpBght8ak%`D$*m#ID|QAF-aWes>P(RiJBfXX>2N4OnQ~#vV?a|*XPc-$aYnD zo;h8WFkeiRhQ92$x2x_yMUlBxs9vq7>sri>ET}>=klxdEn1zvZt5B~SYs~YJ3%Sfs zPS^E%K5{Xew@rDLNkb`rZBkwqxrD=01 zbY9yccd^Mf<^aqvV=hQTBgw28x-}xAl$mdgJL&-EwbhT;`*u%+WBHjj0S1HB*~vFqLQOURM*Qk;PnNlVvdxRjJPpfSD$Zt17=|Zo<@# z^0S$3F(uf%Z84?U>@wx49aX+6&FfCW$m_8*)Ebv&>eje3Dy}N^AI~gvenr*iFg1*6 z5S1cJ_)RpulJY6L`V3{m-=l6yMR{s^&Ga=G8__RXg4oI-lrqEyYC z!oY0Ba$rrSO239ArAN#HDLo>k7bQpG{gOMYMTkZ+UP-x~2sV+dBf}+0)|X6{Y$}-o zrb@PwOaog=c92ZRb{nwWY?Xcn(#--pg4tkauq%gmixYEHIm6YXbgWx=9~0uWT55mk zSVd~5aP{3;txKV5?=FS3f0!en+P!vw_eaGB+x6AC@-Dt^PT@*$E%+I@54;4HeL}G& zQn-$(jk;23ZMIE5Ju zKU;1Okp3BP8TbzPF*6u`r`*ny{s4FubUmdQ4c-Z+f}Ozoz+sNyxJt04z}U*Bf#g?A!mTktF6-0c}4wNomT-quV}k7((4LtcuH}T zUz--`6NBK@th z{C|^t!{tDIboo{LZI}Dsmite{hwcxie_8JTNv2oqSw;HppGaT$5`FLiup(F$tP3^) zTY(+H?%)unL*z@=tnU!7OBU8o57ADtME_A{fn{R>bFS$A;RzD{rY;N#_!)7 z*slZn`b(tqi1s%^zozr(QUi7Uy@YbrscFAnRB5Qv=@xHKr$$YunEJe&Uzb;netoR zC^#?sZ`O0yE%LQ)Uu+bR=I}PdgPO#@6TJB)KSAMzQ zo{paXmc+b#ZNiQ9A^$~!J-oi}IRqb-Xx|@5wCAVG6+AEP*S(m;oWh3xRBR9S0EdG4 z;KSe?a0$2$+yx#6&wzh{v9Bn$V(RN;_o8syFa0*|yu?pXyvHiaGM505? zeJhv`%{Ns(!FJjG7Uh#ucn~}dUSbBrEvZB4%loJmihj-FiY2gJW<{_Zq2tTF@B!;d?hw9yAwz@yJ->0kl52Ct1R`(yo{aC-CbgEW&h!t!nyrpcN0d7o>)x36AjqWk9k%ml7y#biBR*~cyG^=A*}Lwmtn&5ww5{xwck z=O3k)g7YyI+i9~^{w+CN`0=#><)-rPMfvxG)^UW`1WW=c-!$2tQ&tDtd(I8l-_Uyh zr}dB9tGB+-qWY(xJgKOE8n#K=b!^8o?fGn! zzVRAj(07ym6X{ddC=TZB;Qo^ph4@_hc54({HB+SDbSHL#j|Zt7pIhShkp4^Q`+x($ zkxY9zzLEanHHtl$!TnpKgxD|r<4(m_5{be6Gi3Nf(mw`12`*sT+u<1MaSZh!E?J}a zGWZ&}7Tg5x1?l(_4`cgh@ZX?otzs##Hkbg?uP+?4l&iV)XQXezw3qjs^jT{ayE23O zsk|4deb*}X2M2*8!3V%e;B@eD@EPzWa5+f%UkoYFRq0o(Roo2j2hV{~>xd4KB3Znd zLtKzdPjQHf>xGz=qPCNptNjl)SN?2shgqLIF1L%NI>fJ%9-nBTES1}?G_@T_!~I#BI-iL&E)n;F z`#}o7D$TMEzfp*v(`0`GpJIkzho*5k+&qw|#=8KIceL)^1WW>xXRCGd6fhM`18Kbb zIZdrI(RQ2JY8{=%J7){5Gqq6bOush>V7v=pybIv@m0D*a(s<`*`CwsaXmX;jq9}jr`Po;olT|e^YiW1g4YSFr}m_6Z?ejlm<3Y4#J%8t zkn;5=V;)r_^g64Kr&spZ8k>b!)FP*_0oVjg0b7CX!H!@TuqW6b90ZO4$ASf55%?%L z4_pW?1z!c<20s8l1h;^ngS)_gf!~2gz?0w^@DK1ZD7GlN!ALLwRsgGjwZM3=G1weT z2Q$HYz#d>9a3DAo91T7IP5>u^Gr;-GIPNEZwNU+}W_A1hv7UeF{-)PALf4s=TjnFy zd5gY()c5~-edA{9BPC;1d5*i)b8)>+uh&`S;D%&qB{1 zJwHBOFZ6s>&yh0{6S&@ulN0!SZJZp(=PQxNrIsq4C(;!cr>lIHrK@~aq^tH@ldjrt zbGkb3cch2TGjj^hetKhht?T*W>QmGj_alkH^^S1u7rL&dj|bgXWP$oVAT)oS{;kTT zw?pIW{Pq6Od_#}d&Cp!(Ebo**xm!D*RuT@E;+Zj#q4l3jg+(YI~Xt@5~5>*ZCfQhK@rp{Sv!WxvTA> zaU~dDr`KHi$}*jfyHq?XM_o(0>vyXBwt=&rz4z1)%-Bugx)=7{t?uV| ze9ctpWF)HhX`(aLI9MeU&m}Wexk|^{*I}sqG=Hd{X}?ddf%l7Q;QgW+c)y7HPfN^q z(y`qJr1@FnOtn5o_mx>7&CilE)%qNf=4W?js`WV{&CjTu*7Z3m2d&SgXR7r%>gTjR zN6$}beUAD&t2Kl7}QGOP-N@K=zlbl7({HpA{jdOGZo1lB^^7sAN;g$0gfIJ|Wpda=zps$!8?T zNiLEsl6+3`Ny#OWFG{{7`L^UT$qkarC3i}`zFW1!Tf5abv~suVhwp+P?56WNAl6DA z%nFDPB~M9il)NOlS<>CX-jBV>_HkopHT`@z*gtFteB<@+ zPWJT>y-pN`@#RD(`}m~eo$C~*#_QP7_r&b$)Ovk~wi7XKMm(n8Kcn;HS|?qPGGhLB z!TCk8Y+MhrRr(dOF>Yq7^y_D<^jl=(yvSDRXJ*@<(^J0Ibu-$g^P+pUechV&hg-L% z@s`dDzRn<=_5B$N*BR^zZovBVCNSK(HLr(bom#D12l%{LC&!g+unU+6=7UqgB9P7t z;&|{OumGG4(s@BF#P)P>7D(qs@9Y4d7sMyP`QS6)BJeqI2}tL~AjxG+w`l)8jlXWu z|9kcR9Ua$M_t>vz^!$d7>%4o^xEhV|kZ2tbiFDj){%Y-yLp!VEKDM(u?sUA8(9h_4 z)uzks@_%=oSdRP1%r1Ie-2Oi0Ih5T7Ud9HVq$|>*jz#+`g z_EYh_$)V@BTO64s$Fr_(@yId7S;tiQ7agPUZt=3@vt8}iwcgpPd^_a0v;8=2tNV&N zHREjQ>-TE(b3nQe+7D8D(R~E9*Z!{RcvZKIV@d1P_(j+IaotpY6N$&@xVpuqXXt+3 zExwoePws~2ks0>&UHzU<6+EBa+f9|{yKeUPeCT-U@hlP7)h4smdqhcKGWJ`?4eDRk zb?4%^p3=NX)gxd#Zc`fC$8UFc)sN|X+YeGXh>2hmFllyh{y_aq)iW+^J$3&IeIELs z*4Gbrx6d0Weky3KCtbhO!8Tw!Fk`m5&S#;$I)==jo_|!m-$TdK)kD?C-y{6IRv*W3 z*DLG!Qx)lC_OQP7mV z?gqaDPl1=fh*OHi!Dz4+m;sMgPyeXKo4E@WyPl@w=@uS&K3~kSuj6*l&M91aTJaU| zEv7@fBl+)~8sf-lT31!)SB0KxoU7SWT_m^x5@^{I^UzKksc}eo#Usb%( zU=g?gd>z~f?g3AO*TK@i(XZQ=5w#^F`j&}Ek@QPu|E9`$KR5xL11^y)**6aNW$OAh zwo>SJ@HoCB!Ea2%wYO` zk3%Ak1C_^m9NhP-{HYu%$j{pU^l{MrPag+b2W^M)sQ0?m_!*O{#?P2sRj#VJs$4|< z+{3=!X?f2|j+^y!)p%P!HxBE$4*vfjiMdX3?z(!-7-5t{RmX{yn=u$or>y9Xa&<7Tx~$pq@Kx*zYp}iT3wMDE%h5KV9;f>VLC7 zmg8Eknjg<@>lPo&FjwUE$~kI%G<5$3x!*nL#{G0(r{~!euA(zSM1~2c_ph9z=3~zl zix7*`gZq~(qjm{~8z|GM5GI|_aEoQQ8n?*r8@a#EE%slL`|F3x=QDe~s=H)ch6zXO z&xaC%;ReWXO>dFk^KyUdTkPK~_h;N<|0%h@(`}R^${iuP-bVX%kl}jXBEKTJzwd42 z_o@sx@K)uP`-j|Oe+f^77#U`NGwHvuw62t4AA=h z+<$s+mB#T+Ab%fNO|AdYIADEmmC~d2KN=@!{g1+DgS7rf@$&HAYCbp>@3j_zW3lc$ z9()Ka04IYFEKn@O_H=L-_$c@|_yjl~d-r+Z+Jv0Kc(4)J z9HjLtVn=NE1nGML;vj5449*7UgUi5G;977e_#Jo#ybQYhilxEoU_CGy%mnWR^TCiyK{#eE9VYyAFV!nbx)J>)FBg zOwP#bWbwf{h4ep(iS#`raj#dg0NeDvBk^0AH}ME~68sgs1pWom`*B21q}nbHmH}hI zIIs?w05${Dz&2ndhr0*I{|;p$ zME_y(ya306vzWntwvc|(UixL=&GNNwhkh>?d=Dyo{|Jr$fB*ZNV14L)-*;p^evtJc z(k~$qf5P@}Om+Sgh2+0&SZMwZ5nndqhWl(?4*h)9nm>KtbzqqKzKclTcM;2gabSI> zY6s1q(I3tZ3;%uBg<-e%eHVRKbF1&Q^!+S_v%fE9Pp97S+xzY-^!<;!huhm#->>QZ zpuZCvKU~jS?B9pYf}cM;{C8sYhO6(yh^N6zO#63Y);)*4(U1mkF96JAn z!~f5|=c4botoNH#&m{Cy>w1%(KTx=E&%^b5B6|M7&#mFr^KkuLRq#71njaq>uI9(I zZGAqjzxRn7soJylNOe4j*7Xa0T=n}_U*Ws#dLvbR8jZBq=dO{eJ}D#Z-)+4(Qhm2Y z{02M%{=&3>XZ6pK>N_i9Z6=;Gj#A|vILh7+={%tF4jC2xd#5)MZ`COK`5YSWX5U#| zLHxfEpW5|y@@<)?;td&P|IVs&7*FXPMZY^byj!8WV#E#CztHQ#(mb_)Gt&DsPwgjK z=e42hJ$fDNEBuZ@y^#*FPv%Fz_wl`C$ykTD$oV@&RJ7Wz9<9ogg!QLn(At0XcTjr0 zEI6*X#oCw%aT4YH8Ra~i=MW_-MTlCX9ioEd!r=}PCpjG7m6gWzPBGOwzkJ3U%YLEb-7(xh12qDRv z-~Y>d&YtJqy*o3T?3|sMy|a5KAgTHmpi=UI^Y?(nErhn~#r3zr>WhGaty$Um!AtWE zO6cgQM&hr#4PAANqVxN-z+L*r`B?9=z?APo9V-|4%|Cp`jy(EHzyd!GU}Pb8w`2gP zk-fo5VeNGv+@*o<5BP{CYh>(x!5=^qsw^3HM&~5!XyFm1*iW}dF1OZA&}|)VS^8(a z_m#SE8WihAFHp-mqx5T!k^TK*?ta^g%@9h^1)zB<3Uq5CLZwnKp8zQe0eL^)nS=`K zuy;eAEGR7qbgc6=OD)Ai&9G$;A)=BMOsllgT19i_ZP$v8hJxPj(Af}<@+JN|)Qqea zB5t{9s*C;}BK=HjjD?gQi4^}r9@>xcZ(umtVynNy_%4?38H%GVwPm(*N7w>If4?k8Rf%tPF{}htXpBoXAvLP5tU-?DQ?p-+t;2Gk!j%jIs zvKZoV6WVlK;EL0}+Yeq1uKeT3!&`?iufB-hxP3@FHewO7Ex3KfrbM7KR=}4n%voGr?e-JD=mpKpjHFQpDt_hn~L#$40ep4mdL`-%r&_jh4QO&Zcfg z;YsR@`CAN)d)U8Q++gN>)jl9p6Pf0iN_5S_F>LmcCDPe^^@nF;E;ygx1HLYR*$7#R zqOqH8i>cTn#U0+dT%`gH^(T=kkQV ztQ|OK;eBPZ`LkaURXKZ$%m>HNue=t+Z;GE9NMp@@#JTi^uIRWe>bQMo{$YUGL|q_# z;i8Jr0Tw%!XIFUGBWBu}Pvb&9*XpZ?BJKh1(wH45^7szl_L)7t>$sI0^KG3q+p~)j z50T_|pV0kb&aL@muM3S_%cQ{7D?g6P*A4Wut7-T_d^v07G1p8qJ7S+i`|Yj^z&>{R z;-K6@emzNpn?9=IYTNOmwmJO1;Sxl`ce3<)=nYjK4*kvrPH(8 z`XZJ6-DYN`r{~rd&8=FewCs8&F1%SIA1UTqlKU*esix}B15I!Ekj7kaf`?6eG@ic= z?I9jU!k3^VNlc>99$YMkvd|dN%Dk|WgL4Be0~Ai-E$2bWHS5q#Tf;(p6GA}5SSeAZ?jTRlH#b3ba8Fbj;*6+T_pYTbZ} zA8^`Fr=#m0f6v_HNildwDe$H}@{QnNMwEGXX?+*Ya zVVTxDf3wm%*W-OZrY*GNha~y=^>xpgr@52&3^Wv`PRS!!0RoEHe2H{&U9TLyc}R{j zaSt(WeZL9;X#8#NcSD!0lh48hN(^*Soh5F=B>S}#(X$Ou`~(*M!%1ciRrAr5ok~~U z3)?o^_Ibw$s~>Hr#7+>En>uZV4;j7so!jv=!)J_acODYN_1FYCo9Ao_$07QW&Xs3U6(6PI*@+yZn za9{e%DAG~=?y8i$(nmTEUV?+UAzPHvB22Q2?UaSmr2>KUsnePG$nOu{g07H%5gV296Vt^LE5JOE(_&Jq|iN+d*r0+CD21m~~V1 z;tBI%RobT0iaCXWb3WF1cn+BB*n` z@G$#G{G3Aav2?XTT;6w`4DyBiV?&3NIs#vuZWNf~+v1v4U5VT&W^XVSf~?4zuR@ww zv@B#lt>ZB?;iCzCWiuy>JY~&d=&(Z%rWDer!Z5eeXh}J@zdvDkp^QYhJzfE)RI`*G zEwrQgt439EJ|BVQQ1G}a4Ewd!gozZKL&!21z84A+Hq`zQL`u9u!K1>H=A#)n*7|6B z=uXU+z7gKJNTK(p|r3DCycpWHhRjeeg^fG$+@F@tGnr z!po^$^*HOZDJOrH8%)bx50)QEysj+-kX}FC_}-HTO+gJ6UHEtqrY4@n6y6SPJ21_C z^1EOAdi~?Ma-80N7*$q3B1&)Ek7U+YiZ~1Ak>21v`BhOfG4_>x?aPVOsE`w9xwki{;&SACN!&$1nM-k)3Yqh8K9 zNM27MTwbo*bBeki(hjtoZ5R<+cJS%7Y`H@}j}if9IK_fE?r9Oj@mvy^^X}#|lAhF+ zZ(U1zy%yhU%v%)GE^qLddm)lU+bg$T8fT<+6+@hex30(r6l9<~Eg{G&UWL>Zb7Y=( zjnSx6ch>rz<#`(s6uc?WKMl@u=0QB`J4tun(>} zH_nYk=*E2YJq!GrW${^I+oCq@Gw(LI7G$BB*Q5XakI^HX$8SvIjUo8ejhNEwAit}R zmTHzX`+PoPrkrAS-W$x%VCyX`DVDKsfo+x>d1{syEx!8i?cY#L`|YM9LlMNh;w?Ux zThcaG@7R-S&oDi>I~77E-(uDpe4&WH;#E?Sh%hPy@>z_D%odKUUKKy!F+UhaKjm+ik$~ZxRb- zt(a^Bq8F&jCUeYna&>(ErmNR>`AtU&{7iEh+chS=s4 zMU9$w$^?emK37+HWi)bc%AtDH3bg*tMfHUZ7Re&s1Vx3y%U?oX_)kcN4t~zI?r93M zTx!nBU7XvrThBG@MfA|OLX3x*!FCMIJTQ+W`-t1Wblmf_r-ew6w&l@)13m9KrA_jh zXThf4Ml5+AT;g-vo!Ohq(tldc{PAR?;fmJxwzDg3Z!U7z3Sw4$1BD*R9lSNU2sa}y z3~#1q5Sx~LvzYI9G_$^TeSrY)E4+}!rahK)=-Y(su*?qx8u=CRQGkyqS;dvV0fDX3 zigc33$1ocj?C?a=*vA8_vG!xZ&HAbEsOM1*yt2e1HQ3uS5{IVW@Eo?D3;LCd%=*Be zL$5}!NP|n&fTru7|50{Be3PYEoEcWJp7kiLF@>!Cy6-yujT*b}+X07}kB9<3(8{7( z#egQL)x)T)f1s~gEN0b7Q)b(q$BJXs4evO(rIXs{_-6}3l1_sZoZa_$Y!M{ERY8+| z31AzM-j>d_r~C^cLjXoCb?O@U?<;Ai z!&8U4lKy(h)Qvsqlbh>n>9PNa2vvWS)3Z5;dD74Q6&wgI?F5=dO3H#mQO3t$?uhhK zKwrz^e%Z???k~~ThbX`Mx2)c3DhyGqlcQyX3I*^~rW(y`Jm_BiR0rrm2=NLI{1q+z zuXtaAl$McZ8vfDTzV&u-N+jkjO%|KKZEyNdjoI=#^t-OimdO3(`;eq&1>_~tB}2$% z3Ek`|Sl&r_mSZRE@PDgLILrk&2=!18Z5QO!ys&&|V4$jMdMkt1A>+ea+RrPv=WZT$ zt-_S+W#5cU-oA}XG>JEPYZ@Q-Ht}A(;W2m9c#}7YiKeEBrf=^V#>K@a?&nRb{J;a) zYh-$)$8(S8Lpg0x9p1?spi}hc9|aa>Z(h6{b;>A@aN%-P${*~ox{_KlEP3wv(c0Nm z@g}VC-Vt5LW=QvCDWXMi;jXTf*eMh$BTFns|CFmF(D%L`_qO#ZTyk*riec(0&4n{* zUH8gY;!+ph$3MEvCpKiBY^11?EdB4z`^iMoiBVRsK#2po)RuIwy?_-X!CVh`<)Gug#z@W_;Wr{jJ7b7Jcmv`FU}sz#Rf=y=EY@I(8bjsx2zqQi?}-m zLuHcr#No?I>`=Fw_W>qo@+m3nnMPj>F_aZ65H(#WsRF$j7(AzLkpdh2Hbz?1QDd2O z;ol1Jizu---Scm~O3VpX4LAW-zSdL+biLZbfm>r8d*oM;4?F#lZ_|v8=cftFTmBbA zztNd)-JitE0-@aZSZ;1Um~?==WBIJoM}JXzjX?i!-WPi#miJRHa5~F%nHVo@B>9dN zwQMV;*J7BPwm)`#5}-66HLh9mqy-b$wJlD`5|A0z>$ijnCqoD$|H%{&!k?AzD6@FM z;U`(;tSS-P84FvcvK2ztBJ$Vuj)VK0`cf;opIOwVr{$I*BR5|4E>&bANzDhQ5RLl= zkpFWex^n}Q_r()lx4(%tt?M~C0fJ3d5gP7N)L$A#eA2i2#ybA<6hUMimMq$RqmKCV zV*4rXnhjLw7Z=r3(Sg?1;dQ6)Ebf}SwRJGGTdRpZ0c)}z4E9A(5h3GeR(A}`nVTYN z8SZ=r<;)hH4e!a%l~jz>uIPDOniXS8Jy=6-9H5BDNk)oqjsoO#<#!`9HFzqiVFqi) zR%Mk`bsg1=W5!AfAy|i3G}Vf}6YP1hf0QE~){ahAM0biR6{he`Zn~$azZdc{+2G{iF4Sc92HuUCoey_Cz=!)+j;uVdtqVGC*a`lVG&^ABN@&xw59?2bzeTe+w z1ltc($4p5VOV=OK8NMjhU0}t!$L`=LI1bP^<2pPtnx~(hqs*tP2Y}Vb0e*}6>r0CI ziX7qA=@BK~kxaG5l$6>&ly80%X}vZkPc^TNgcA54xDS!mMt-tM3z(mKEyJfLDPy)< zNBj5bw+rfQw*c&~H34C>L$6db--ZrkYzpyXs&5g5$(9G2lVy$z_PRii9|5Gb9{Pq3 zyyIt`wJC62Id;NZ*i=$L#im_%Erq_(n*aN3UH{HRgl76jZ+LUxO(_3^=Y*MJr|<0G z^J)Ujl?UFeb$R=!7B!Z0k-7$U~JoUH#dJ zOYHG*#Cw7T?`#F41xC4j%A(W{j`VKxP4Grv`;O|H#BkQm?bxXT*t@aeIb_@7(2u(* zqYpd-e!YE8*`*aVY+yojLwCA15Z;l|^HaJ20&J!n^IXAMdu(<-scmB4qx#A`$Q)D$ zV7+XUzgaH99J>1b769VyArK3L4Lg-v2JDaE1%u5 zX&q6{5~g%Fn9#yzSe?0G^Ry|GhOTFsevTl&6g+tO=Z$&ny$i(GQ`r`0BbO@UmH#wl zeDTCSi;ylKl~9T+LPJwme9T#ulG+Yno}N$hmaLTFtdHBeTAEZPz>uK3`fnbp_)<}hg$%yat?U2jN*V^{r4JN8=%%!f@nKVC++>Y@S}LsU6`P}^3> z&8)AM&BW|>MlKAUtgS9=sPXWtz!T(GP?HfZ8HmTUmV!EO;-tAr57mY^Ynv1LE%fep34p+CN<<|=91c!&;WiC5T(VUm=xr-C>EDv{1l zYDul%p2f|CJ3XyFUmJt(F<;HqBm2qpXaC3YLOpV2Zp2_MdHw9u8d()Dz{HQv997A% znn?~(9#z3nFn?7VXGQoZ-L_<{Ow&1Zg= zPWzE0kjY;s@H zl^(PPm3;o-f9$vrj7qB+KNShccfK674ppmxs76^)CB1LgtTqL+b92ZM&9G9Nee$l3 zMvqouDI+Ymr}ZVXa{D2(z(rGOn5olsF(()Zdp7NvIbl7~g^_ob7{q{VMs;%Gs2?99 zRDrWL436$6zi()#g!)+_OwwT+?sK!`%Eyr3;V#|TsOp#WwRg}PEN!#N)SpX)3SVRa zxTfHFjzwkxzmgW)&8s^pA8(^e3Vbe;WLMPMzN7VASkC4plOZh9%P{Yi1AU@PX#k$J zkd3zg#Hqqw;uSyBkF>g69+X^+;ar6nVUID$VTs*agTjkFO8==p-)y0N_n#kOhP^<` z1+&Fl*MyqcZ4oc{=81sTtK1mZ6>=A2XXs)x8=&J~Ei?s(F~|o!*+d2AICB?2nB)c6 z#s5Z(Le?<$pfB{r{wG<(knI(MnJ3`X;z-k8EAVe+q56?&xltyxpi`K;o0u%u@FDw2vMmgmvJ{fp0>l~ypzb!K9_h5a+ajKB zt>MsSt08_1tQuBX%!pFK7}h8QZUDin0H2ZX$P99wD})n&e&EPtv1MCGwmSJPLE{23 zgYU*&i%g{NLT6H)hV6HhHS`y83FX$O3{6Q2OQ_2cT%z(N`b&SEuq`?1?}*rN z1YiU5=I_TDuqmUTihDaE7X7rXciWqA%PY%QvO%D2-lz@SyRAhKlJUDM`3`B- z?2_IlQMt3<9p;<1-T1MgH5PxQ(7J&@{Lq$d&JQ_B-G~*oAmAEKmMs8E+tNz zncwaRFAC5luDZ`O7KV|{3G(8Yy>o4kTgXqCkm^aYe<4x`*S1R$w^s($8>jKNkM$2aXu$-{Kj!MjjHaeJ!T4uH?Bo#VuR>{_NfF5nw0!2$wgZ~&Te6K`e zvV}sG5X@IwQ&$VE45!EdiJJ5+bhJ*C%zPRneBQjdkVe{5T8~^n`=CB=YUZE`pE$(- zkBp~dlnj!0b76YZly$JK40#Umf;nWnsh%S|Q52`tn;tf{pFdzf+$)sj(kegO;yrT2 zogqD19p#JQX@Sc?{P44RE>9w=LlZ)nPZ#KFj>NK7AEn6V;75Os=u{0+TU_{exR&1j zfRU^SdWXNW4J6w5#m~!HoLIj|Xe(D<@QnBv!kik7!T#~*iFz#~z&=C0t~6=IgrAPe zZ}2W>4aMtr6fj0ynagwu>Q##}BP05EObI8*(GkutFJ{TBFyKsdQZn!wt6wg$T&M6a z{j_Ht;9j_B8smI)&rVS56Z?7i^Sx6}5RuABn(v?cfOVe8_Y*KJgs*|p`vszMo;5T9 z+P`zriLM)&v~tQRVtnYilB*&D_?C2+9#I9}bC4KzOB23ch!=mb%W_(zpndsIVU`c0 z*CShhm8Xr-2k`4MYmlGeFH{OJTnoeZiFoM+(h<7}vP+t8VL)GKI=cU&c}dtWs!|?e z`!*r6JetpfJ;E_RQv|ulbECXO)&XYWxl|}<0(t^#I=B43Uj` zI#>&YeM)P2+u$R4vFPA<+P-*!R}XJ^JoSVeGS}Lh&6&gZuPu2_eF9A?^@`drBk@K= zXt%M|S~Igv)CID(>sq^RL0MZoic8Sv>y*uiU$&9VNQt0KC-!Z4V5SpjU~Ik$`zvUU z1742S-9KNY^!K#?6sq5Tz6zEV*5ruFVKDU&N-K|G!~~ut>^m!P1b6bo_Qi0vWONU`B&| zLjtlu5Cws*)zZw(p{!Z~*l-Am;;L=o- zha)O4ZPNfjeZpwV!xvF#0d=iNLAZ4zux^gX)k|Xj%#?8&#^*J&x9yxNaC^|Ega(1B zY!LYy%8#f<+qu7V#6ui$Z9$eAfJmwr8wBs2TZ(-KRJy7tNNJ7xGetfW7CPHM`hBh6z{3>- zQucDx&+zXY#Bm4C>g&Q*es}w=5?hls{*d_>+?yB>4YdBzhM^Uqusq~hd3!Q^P zTyqXm_tp7#c3+SmS|sGn4lMZdQFphLR~ypum~6C~2TYx)7CjGXzqq(s(1{sLS-qn! zd5-0;C>T5UZ}Y;UhsUYVpg55w05ITV9(Tm?=7?|jo9YcYdYob|Dr!|6tU;ZU&nSNl zq>-P!Hm+?gS6^lDJo=nM2i&c-*@8MbDCJ97Aveg{2A_RgkqK{1{RU`f4i|cID3CUZ zCmX*)=d+SG3qIwCm+}ZYcBgnWz9G0Xir$=Q>UQsx7JU<~ zD`n^mo*fWT_>d^qE%M0JLF$tMH|Y{a%;3HxrvlJ#eV1;NuRXH3`0s)gSKB}Gul`2M zt7jNy!kkm!4CF{oZ99R9o^0l&xO6Yv2;(xN@IVNyl1YQ*myr#_*gfxFf7sI znMZfxJLs+s3;6z?gVeP*oFD-@#Pjj|x@km#08SmZU4d)-3Hs?tEv9Q)iZK12hg%56 zWVWCXkUPs~Rd9Nb?&eDtUgC=OBZjf;mjxtIT8}c$!X;U6zDYynbWFNW{b*B$w=m_OHoZ9q1akCt5A3fb`>fHkEAn8>*96dTl2FG2$N~Ji+zN#w0gOJy+Rh>a3y%XRh zmo`o0jzC^^nBt_VyDsCef;~HOnPZo`p}$6NaOzI}jK@YkGIVf7Dm*h9A%kPTNduhE zC+$6qWci(=bf>IdQ|>Xrpu!BK0&=bkB)aS#L|Ll}s<_q|^OfSUQJ+3G-phD9&cjJ9 zKfjp zGQsbP+o?gc-zWD|x=Qs)8P_A7e1-iu6;q$IE4CIEte0q--}OPKKpq2MSgaMIXHotel$v zM89T-XfgaXSnCv)q#8_PeDOr(R9RPEZ;P8GO8iYOrnDGk2=voT1StG=sHh&RXLNw< zGtLUnVeoHPGKuL|s8)6;hM__=1b&W$^JGe#p>*#=Kfd3TGD*BVVl0o9FU1VjFGJjNTh@a>rqh+M$4nT>oX^B7UVXlCJ;n*Ju<@5{aK)a|%6)*Ug-^ zp&w+gN0_@6rL^cgMHUD;(iX`F`2!eSG$t`X@T0KCI8it(4lXz?sJvzQZ3%B!nQCND zB6n^&wMZCrgRY4C8T_U$2nZXryA4dCPfkD`&c)4mjmBP4eA`>53Rg;u=WZ0Zindq8 zso!%rf0_T2x()Kmt-1zNtc1&*HmXGbsaJweVbiIuaT|E}#m8q~AU9}Paz_&$i=+1i zhpTWKcs;S!qXx`=Bs@{zP@E_3bwV!c$Sqb1YuhULBekz*1JCBWSpl55wg5&*;Fwe{ zwEpB?@0YujXlKdm^{wsuOjkoKg{8O*q>6A|{68W-rC1D;^o!#fZ(KQd(PEU;b%|u61+~FI|1#tBa)gL1fb6O7 zBdLt#y(11Ti^kQos2{N)(4)bJ} zIo=eU?Xv4?e9-(PswniX7AG3muH)!DWiPF6gBGIoR5($ffy4?7_nj`o}q9>wsKs^zlK9O##nDZ3h}{e z%e7c3L2;itl;l#~1j@oB>_HqC;dnXo$Z&=UnXJc?o6vgzIr9NOa&Z6US;F(F@B5}N zZxiS~6GY+xuY=O3pvmt~HbL*I+UEKaU|m*(kVy(VzDn4Ose4qJ{2!$`1RVWHUWZ6> zNhJ-@F29*rc$SjIqdB8pK{{Kk*be~e4<>XxKOBh?f{A>UFCs@A-4Pz9>u`zlu0bRY zqs_MX48)AwVEqnhcYac3``Iy%30MUgy#%UZf7;!FyS1(bJFf34b%%FUzuCVbcSM@S zclvJXEQU*5jh*VZftOE}(RB`ANc!P)3;mcLST0r53Ce|GBWRrd_*g5LRCQbHw zR2-Fk?*8u%&Xcq{J8tx1#nh}jQmBQ+dm_n;jk&Y&%mJGkvT*=8-dC&pcAU;U73|4< zu57R74D&{0h7j!htvc7YOfO-jx^l^}u5z2|9AD zi+d}dc$3DNn3qQhJ6Aqhs1!YH8G4I(?Ljy%2M~M(Dxqid$s$G|_Y+2^JOP8R>i@_5 zFRCyPy}yqkP#oFgys8(DHSp@2`xXJ0+0=lsy+`vo-z{ItP~jHM8av40h05(l*ts#G zNABC{(O`%*vSa1)X^Owi0ba)p} zM|IaAB6{ZO zj&d)gyMCW{E?7x_{eNU|nAA?WHrQae7@o9+zrq7aTE$#3h9qsVuVga zzt2Ml1G2kp?fDUi?a3jEDkxKLCNfv4zX+l`n5DDkU_qAVf41+&irbvbwu)JXcAs*yYGz7KG>eSNaG3#2!_UQ^Kcbxk?CqGv&c7%QHXUulCIlS4llc^h!PQJHq^)Hh#2bNE^G46VpP4Hqfc>rAPql#dibCa0IS5* zh!r)To00@#&g&1KzsQfpzrF00G%uM>3Qpdn%n$O*ffOzOgbU8CEZR zzfh1uOAVuu>{C1S|521f>~Sc3ghN002y^au@9kDV=l3kXGl=M<(3sHtQ2cE>`0OPY z&S!*vKP;m@h2D{l{2m$~`YF^^nqxY1pvmFGea%XX2%xrSw14a;@m*-WzGi@AkhZFz zdgG1aHD31v8WY%pd`(sIY_rl27OI@`=J{i?Ns%+gnrsr}u21!1z@V6^tY830@65~y z&mX|_Ehy-hTdM(D80p7E3e~P_K-rIuR5=C%!}O_&&G44u0AX>kdacWxK#g%F-k+ze zcc0xGDLHfc^vquLs=qPIRI>%YpNt6BZA&5bA=V(Fv5#U&YPX;xmb}$!BG$wy`Jd+l z<)?OMo;jkpy?y@CHLuX_*q?5Lx1FiWTfjJNmxrWTd=E*(PmVI?c?~_=B%_MIoW~2g z4u&bc{?`#+sRq|Fp4Xp3@%2%MDOhgts5RzR>>7f?`n~t=7x)s{C)pyCwE4Ll`IS7+ zzY?_$q3NTYw6M&@Zzc2}l1rOw5UReDc!7nk>i7tKT)!QlwQ$&Y-UzLI$da)Z&Rygp zxhk`TKD^C{=kNto9|1h-g&n3Q6#Wy^g6j{HP8Sd_zga+FWaJhOQP|l91{L95|e#jE$i<>&F?xNsRwybU}ryw zda`ATZP4oWWZ<#N)4{CLcDlkm_WZB?Oe&V^GXqQd7<(6%O@)bW@Q<-9^l<|Jh(6OG<6$ozUjumNRtUk%*!wd*>iAo+b!I_~mALxb z2?joG@qiDv!O(=^jEotnso0ZH>=?=3rH>YjtbDhlJD6s5onBZ4maC1nK!cA>J!(b* z_4fZ`$7D$`3>IJYKL$LY?(rM3xPEubzDMbY3KoF5-7c1pH(yqwe*h`$BfM(bMVlh2 z2&B@MlB^~_YJtVWXmsLbP>WDF0ocbpLAH9#E znY|lL#-WAEG3r@GwHMIVSV^llW}HDUza6Rn4CP3#SjC?Euf2{!i?~KEI*?O;H7nd& zNEa=O8;D!suLly0y9P8&;1+iQmJ@JMqVRG!*x+Q%!Bv7qHbKS<)|m`&pHHj4Uk{R+ zEzQ4I4u9_d&V1kY0tz&L@x<4f6aev}hrgI3e}e7n0Pa-c&Aiz)rse=&Eza=*R$#h6 znI$w;VmfJE7rcrP!P-^q6aB21rkK2iYX|o*7=XtdSQWG)wgujWwhg?-Rx>50LD8SH zZ1=S5*`}#sHqG>~nT6T=KBb#Dzs$O^ebVjn*>z?_-iz}L8sQ6dYBpKhj5akpFcEMN z0Rq)Hp(kkTVy*kRP?+<9_B7VwJ*Nu=-h`U&w+8=}r0(6EL07P1ho+|Fndw95DPP8u zDOXq>9Qn2U2;&J+Qtb1X_8Rom%^)pL7}Ud+n}EUWY9}x)H7A(#oD=vp4&$_-_v(R~ zkfP}d9RBm*VvNUj#0(0R6H-`(K-vC}(=Q5BiQs)^923XLN97dF|9wg!fX{n}+2L`Q zYHahsPdQShF?|92^1{3cUgb~4=4agL+ii-&)Y!pGwST1XNK;OffvsDazt@v3M2s-Pi{6w zNP2jHIR5g13I@Efq+eHJ)^I8+JLhHST!Tp^_GZnGBmgKrXV7t%9$a?;k~oQ*@aU7a zOVjgM{~sCBT5z8MldN?}m2$u07~R3uszf{u{CG1WWxeqg^TCfHhR?vBoQOr;(o}AQ z^%{B6uI4Z2E7{ZPM=C(Sw-(a=dVE^hq94thm|>6b)kED|q5vHpsx!(#=`(O=m2?+a zz$EKvo~2eQv2-e@{n#{Wb3nr!E#`7QP-}swreKV8FZ zu?n?gw&2qBl^9aN&Dd-aV zNxWqx-1VMOwld#8BD<-X-I!kmJhW&LxfKM%%5>l5souUMtNUdq!?eUx%zn1P&+liQ zf}l@kjd?C5yx+G4Mb;=My&A2kao{JEOBf^qlsjsE8l+dGLK(yZX9b64eNB|;T3 zef0?i<&f$w|*UC8{)b6R&<_%+CxUp--Nn_|C&oDX*FezGKb z)IP0%f9*$|!%v);V~d^TU-~dotSZs{u?Sv5a;_38Fu`@?x2c5drX;*rUz1Xm=(H>X z5AS&n{X4m3Ww2DzbUpZvK%#b|@2-mK)}r@tjh9YF5P^&JwpYJNV;3OGkJ}l^^3qB6 zxf5n@wB)LRsd7ezTm{6N-36NC{yHc}il!t4DXhh9t}%Etutx1Zlkds;N3-G=s#6El z-#chVvbAnu4Fa#J*8|akr~Q3lsVjYJD99Wm8gDZ+E?%EN{%fPys;vz`2inf3uux-4 zZ{_|Ct+GX?<_ZSc%0ms@%iNm@=*C)!e|zD{a>S0*(f|it*Bmi)9mSa}iP4;!K0pWA zZoOlZLawq&4t3gj?C_y4rSCz>7OGhb>4RV{>-qnPH)Y4IED+H$FBaM^!FHh4!qL}W zl${a22lctt_NgT~67UuNOHE~nn)54y8c%TD-R*}wYq}ad*Kk#$U#P?-dS7GeRVd}^ z<`|k3`ROr*=Q5Ms9gR7`#fa3%)7jm>KSE{<;7=&>GH+AT9dskBx47UY`C*bT4pZC| z1>0Hv#?kTvg(QcZhMGwN$lv-*B_!1Qsx#yYGT9-gbfmCg53d;nd(*{Oa9aGdudm~(dx5C*Vp;?<;<%pPKH&`c zxW#OCxEvmJ=ifof~eo-;J@qY|k@NAg~c)U5^dxV2sl+=@-4Y3Qa@iTNg@ zcmatH+TA2t+Y`-})2C3}g?;Wtf0+tTnzU3~m2M=GCAi{ghSCAHp5dTzABQ8N3q-Qk zb1~BtVoPMK+1=72-jOH6r#-__-D3Ipn>SmP?qMZP6O$rh%SC!6l*XFw^-crEG&{s* zz|{*94Uor;p20Exr2?`Jsp_+El}5W=TF)*`ARqmr9`-yi(Q^%($8(lPpkS82*d^rguo)(^FTo{OB5;@+D4PF3OKrpARqsU@^bhh zNm9g{iy5=O1}I8hHkVJ%9_&eb-Kd^|>JoCD%j?Ci%qUA3;@0!$C6^wlvLv3Lbi(Dl zC_7ch>`v)aUGDyaeZTMBB)$~hI*9*Q;uZCmF~1tL&bgAakg8{@Q4od28KWBX6qn*$ zhXjaMW9lI_-0qij!cX*=k%Rm7C>{5pEth|!+Sf_Eu4$#x4*nuhItqWiJOALkPT+6H z>lS9dFme5F$6BlR4@~9>UzQl(N*A;Ei^CI*T5QdJj+96D<00)%WA=B7pyFGSpj2Sf zxnABqi>6MV+iZ+RmNAD--NyL{p%((2S9A|Fr&6!co?p?fSnfo>vah(xG*whN3Hi)x zg8CpAeQFi0)OP=?B?RzXlpA{Q6R~vhQ||>z&aEXg#rOO9V*)Dc2ulZLn|}v9ny8AU z`#9yQcT~q7!<6S~X_gLJAz#LD^X-;6mxF=1H>t|KFq;I+{97F&Z1YH+^URKWXO$KM zN6geegdes};IwF{r8${bW?lz!iig{>daZ-v)|yj3w^;=}SW6Q7P5O~r)ANexdzDcW zZ>HB0Z3IULabAif8bjmq`*Djuh->69MLy=y6xiTO3Fs3E(UggvX4&;m!(UR@jIjx| znf>9NA{M;&DptNL1D;@6I05Vn1&dhxBjJF)q(ux;iaeoJwISWr`3Bq+FT02cQ4*^z zBtLk~s+wrCnf~SeiMYY)ZA~mU3{$*}BS$HE-e^Q@VT>{Y>1&7PwU-AS$WKFC{r`4? zVo2@lIM;9Was3f-;{5iUzRJA0Uqm?MkLeVsmk|mx=wgHx+@B@pwSsBo0xPuB#93xe zc|=j`Nkw2{Di9)J-(iS4Nif@@#f5xZ3>#Z012N?Sn7@N-XmR6glf;C&Nw9U|kWa zl!91=JQ?0)T;*<5RfWKp4_U{>6H6~HXIyAxt3ux;84ML#o8JVwG2M;yWgaLIq#YKyoGi#zvgad)J81u4 zb15uO5O5S#f|ew;^h>+PsD#kSo6cnpo}X#d8XE_|tx*Q!MCP)u6jKB^{g|R0jugw` zuQ)HwJ(IS}AHX{KBDD}OiSbUnaV4J39odWNdECrb&$dgG*$b(46ONS&wjc-9slxct zPb#+>4w`LRVTah249EF0;GO-vX8e9good!A?aU85&9=|fW4RxOH5h;o^N+Q4BNC*; zBsgCzjBb}qt-e2bT z1FjYmx+MQV_kR82uICrIKpCcva1xQ*sjf!m5NF1p_ZB*}%rTCM)=pJQhEQV&>Gyhp zwb7mWv#7mxDmX2Fr=2R9451ejKrm0nGIu>lYbS-f9wVuVD<(YGt!2(~&cY}rj7iS0 zj+l)|xE^Gs<@=E4c&}mPLR0de|6cG(&XSS>*4A#R4WUYnJnS9HPk}_6x|euwE3oq~ zg8Sv07%7B)BgLA=1Mdtp18;W=s@4}oW)F<+l)uMbVq2ci^6rXZ`N$(Dd|8H>J6#-} zX9!5Kf3r5_j~OK&lurCW}cQS1hm(Xjy$M!4(^!4^D zB%+h|tXV6W6|TMqGG?Z20!k$I@5c`@y_lYw))*^6VYMbc?;y=)X@n6B+CkKvQ$eo62A*kg15EmXE{U!Vo3PQx z@&3gtY_9<3>I9h7Oh5rtu3>^HvA zc!r^Pg(|#ca#Re7oiJqop^T`D(FV)9jP}pPF$lW%m6Slu;no-GqXBHV(k>GFZJ(4h@n3YT_$@aJHER%bn zVE>CkQ84r2UFMzwhsjr2d(!t9i`tpFa=oN652WjF4-`C&D;5LA0FwQ~B5%Tod>$S;!MM$W!TZcPdic=5IHo|Ui1m^x zSVdBGB@W@sxO-xRlI+4|UXGOF1iO3~%9bHX zxcm#x8<2Tjjq|$&I@6ol@Ef8pVqYjB*)<8iB`GYP5FM9zm*V)UHu##hyj&EtHxi z_9is;iU=b6m-qM2=YH17mGfNZI_DYpbANjhwugcZ`@(T^+CLAazr%S(W`PMcv3C2M z1MO<>;za)TdG-gm$_$_Z$ar)|!-!1q?*I%5w}K$6t@62dslf5%}=!y@57;B*UJY2 zZj|c+lsjBa20cVE#S6-J?%-?jj+tEuUF8&P=+Gc=i7X7ou`~!28EjUTqezq<%Z`0E zo&kzlK&-rwdQb~OE;iA?fdJ-!LI=;W54u#p9->}%5MDLG9MPV}3*_A2+b`Zyt3h&U z)QaFmaW`tXdhz6Wa_(pr=L*Nb7 zZx-!7PF~$0Pt*~}?E;>XsN5&R;rp@g_PthfZ4cnyP3*y{$QPHOCnZeu{&a)LDK0F- zMYf0Sfy3~2;Wkgxwcs8zDRMnlY!wVpf`$Hels;!LA8mff$asH)_2B}#@teV0u^+2=keSCsMt0FZzb9&O`ifv#0lSf6J z@|d%oD^BnS$J{;-sfr8Yn*KYN(0&b{89_Hz*RGFgwcB@asN6X0y1}=6WiLHCa{EaZ z&h9iIuDM{&Px|}A%a1S#iQPIu!C^-aPAxOKuIEgIchuO*puRDTP32UT8sx1Cp=L-0 z9p8b!E~HnWJD!+lqpL%kTZmPq*B{*S-(U%1PuQ@eW7aAslv&-jRBqo z&>|ZzSzeg-XI&l+Ah7fKG!Jd;!{a`>Vv(K`;V<>4)Y7*OnQE@Wy; zL9Os1Q_MaZw5FiJaW&c?PZ79AB6mPnS z>w)&rck@ce{;IqxjUo$BHJqQ%^)pdbocHl?t^et)w z?vN0$T1q*9utMP%#;sq>y8dZCy?5vtX5iSN+tn>!xM@=ql8L(jGC(BcF*7^l&SzlK z2u;<)4jXbhP>Tk8=Wy;nX6y@EuKx+UFWat3S;r z6O}y+vZ3)n#}19IesIkt$U-ci!5l95^m-=waShwwfPYI1vFFrT*_I_2aBboFhd6jxWD6d7719~gT0pyA=FU{*wt2LmT2dptT40|d%eiKuR#5+30xe4nbg{;qcp zVFaGscOb^UW~0h}+T7PumkcuUFcQ#|^0*3%d7;^hEo95Tm9v^fhD9bmu}=KEHA?SBqotVfDH#c~C@H(l65xIMiQk&RR3PscL{+ ziMZ#(8}HAlJN_OZJoUL7pw1fzUBQK|1WcY$m#YnVCq2DF3L{tHMIY1)xTKwAwQvZsq9%#NV6^6&`|x?dp}iPk$Z=>+x2L5G$6VcRn<@T z9_-mQzBB-y4iKcF3hO+C4cxFXiR2V#jkwuAk|y$bCuFUtYXg5Intwema8uXjcs`_u-pjq?kVi8a5HlVFc%q{5 z>@mIH>D@y`<1+2UeeJz0u|B{hn!sCd7O`>T-E-$phw{Cn%WnBsN!Q~`>2ez*Ige4a zfpMJT-d8v82S`?-tOVmd%~s-fHf3Rf>dVr-bQjO_GtqW$!`2iS$&n?X`>*mKVw<~$d$Dk zkYM%VQu^){;dkn}x2bNV!d2~WcEk7V0Jm@4gJiO&9+}%;ly+;YoXK^pY zZPV#qnMb7jF$3L58Mjy2MPWV%pH9h=i^rp16rh3EmPC~W_80{kb)RWM^1<0hUZ=tq zpp$vbS<<^o&Km)mi+JN*upN( zj;wCPu4+~4&GH=qU#Qt5y0<%>Z4{HH8;3jLZ4~3Cn+cdTQEUAYDDI&kE5AqzDxGxy zRes{`u7}vt)y>Nn1}pw)hw2Y9iR7^#-{@N7G<~C94wxH5SeAi2M?c69?nxb0o8E|s z16L|V2>0ji0Y6nO9KZ2RPx>`)S;eNyO(<`Vc+?-Z2N2v5=q;S-RKJdV)~H+Nplq`u za)ZK?V1lKah!d%V-BgSN3Hbh7rb(%4hKuhB_8hWY#2m>_^9TY${#8T3$^jfV$63$L zRZ+oDou1NN8N9&hKXbX1m{-l7Il;JBs6PDm`0Ad$y7*b=*|g`A7P~*%@P!0A(YT!F zzUmCrF^$Mof{8tWa*Zr03zGYbihC^D*Tj=6zsHmxgDx~i` zTqEhUbJ)eW(&JoMf@^qJAMF*q>Vrbh(HE~~Xa2w43` z;9rqKc%I&bdj^QW;79Iw@ELIxQrhe>S~jBo2+z4P;4`=S`EWgSiM@x!!F5z)eT!w{ zhzqF2q~2@=7Sen2U#EafO`2vLO1o^N() z?z5>Bxr>sSG=7PGZh!7`P`}c~zWR^)b-OP8vJ%zptJr3bn}+W!1g7{_IJH(-hDVvB z2L!P;eSJ6?8)_no|H0d3zi&07yb z=$vwN_e$A(j3ltWD2Tlg%0A^7{gTq5Jw%canRpO3_cH7E16f?x**|nZv4?IS|Ij(D{i@9_$i;wqFLQc{Fp$1*J)2~%Wnh?7>;2vLeyx6o=Ty$;rN5LFg`QJy`~wMj zw+;JG?^$AEMtI~t^j5(FKmD9immLpJwZB%(s8h>oZLFsXgS_`8I~AU4$4`Aao!uW) zWGsHXSvF~|lIN;67tYNE5)C{onf zsl$Z#KVwwi8I{+6in^Lr19cyKZehhyp!_Gs1^b1O+D-W(@bz}$$Xkn)zNf%K zRh3V|>J6)sm{T7XAv0aY}VeC_~p2_i}kW6!DdkjS5l>gV3n#Kb@Dvf>kT{h(J??_0e z*WPtXxKA&mEa;S*(Q^@z_h4d!0dwm|`nMRvof#X44oPcdpaQ-^p>$~|U)o0Cc_NDRCo%7a$UYg|!@Vjv@X#ojF+3%A+^7*#J8s=W@kL%>E zo66Zv{|HaGa*X+2FuV*jUa!e|9 zR;M>B?}*pVGYZc&gRWO`ERB2Yqt#c|FZE%`24U*OSrpT0b;u@+8?*F8A#F z)-d5M+@5+AleHf3T&w1Lw%7c^?TJ9q*=rFFnlOXOi)K4<{DXLi__wR!z$xNvY`NeE zGNK&2J)6f+;NGwf{q!hCzfh3sVBO}y;|I|J7;=j7j_+-{3tfqLQdvz1|D z?8x%;@1Zl+?61VWMLZ!#;LoGNAH{A-NA7=WioRz5CR7OJ@tFUWa8tp0zYt1!$2V2|6=Hti7LNR=xkwueokys$gvg|1}wUdcQAkv85Kzsl6#Bb*U|-4&)B~ju(=qH$3aW`QqAA@Uf&r{5 z!p3w`YTxsP;7Y5RoR^VD& zu2S7MO`>N@4_3p-``3s7w|`q!GC9f2()(F`T^omiFrd*`W3tqaWprP2<7KVb|~ zd*b#KOZf5!VWx>HcOzJ__i_Uny2ON^N01|NRVEL#VDazmC%3OP$5aDZdkM6r*C-;U zF#28FP?%&}*X2kYwvCH&QwdNL8&UHUiEf)G{y)>D^CkmT9lPuFAJcRW(38S#9m4>i zeBZFy0&=q?`wD_Fc}~_3qV!C3YMyj>J54{FXts3wedv{rA+3}B+rPVdnxg7cvDcRX zV@6j>qA324^qO|v2pPo^l+Nu&xM8$U-vnb9brNo; z#H1>w}Z?Bcp4` zm7XIbQo9z#ga(@$GilplRfPPl;iB0*iQW=aT(uwY7ZBZ7X@U@*$)RdV3ua!_B{g`L z(Jz7<~0X$Um? zOj#Srv>cI11l~uYswwU@f=0H&OEwxsThIN7Okaby>oI6%QF~SDLUd6D?f@+*l~F&saUkm%{UkNgx3AwrZJfhp}_&jq;B)`$UVw`zGZc)6WXa9dnJG>7_3 z-!kHFqcD3x`V3C2oC5NdjdZUeag&m1_REep%+0SR@TsGKI^1!b$Cr%6@#TUJBq`k( z{Eckc&)JFLMJ)Yv;o(A-j}&s=t67M>Q6AzEYYm~ddDcu-b>s%pCnT|O)FVBAY_xsm zPc$@QpWnp@ZUpT}52*}%2su^|6^i1onJ^MY_OCgW-;Pp&_CG6DJP63X-?;q~=~=+m z#nunxgVh=_@+2V4%K`eU5Y~lt&!K(R^8m1wF79&<9FRWT{er!r6Pjf7EetZF@*7|D zDopj!=ML1ny3|}c)nahlne$K=-w$88@3YZx-Pa=vr1KgyNiNO=m%yIlq4Tu$h?6WMF{H;I51mb?fKVB|hc(xGFi=0 z)t7oP8Ly2Pcyb5+e4C`}IaU_7KEd-45`z+6#YS#RdkbM3-vp2!n(00Ts&g7cy%tMq z;QE)mi%n&f;h7@WLarcbb7AjvzdM$fYP zmtSUp%gYGmX9T2QrDjE&GIHP#iZm&frC*-%UZpi8XP)}aWqj6zS+UTku}JdUZGZE>)#b?0UOSHi?c=Zb>c^ zlCqR|Bh&Be$=#@X0TFETL$B@>OXo`aq&O_~kHHXn5lizpEX`1e5|}KRX2dl|&x6My2V}4vVNa3-ge3CF<%eYb*B>BYu3Ep0o1W&GF`5WaNT>2_pRi6}_3gexen>=m5 zw*-g=6{1&+7%Yy3SMP43B2G6bB-5yaG&d7wln#l9y{lQIl zH$G2aDfXaUOik)&3t~sl=n68d5U5u! z!a#_7pC(4#Gqncodh1uxv(M1cAF9+br>G*>Kxi!)bMUTw*w(%cPdeSBJd$9 zM|i4b;?QWE{h0w7DsOk{F51QZJfT^q5cTZZn+{N@*-c8KiW(yAFPGBpi+5JjJ@*_{ zle)sQvH#*3z0g;7b2f)*CcV8@uIwtd;=)cj7J?c2!J?hHEF1S-0@TiLIX)TdzsSpR zAhU1s#HEn;Tv*=D?K5K$Ixw2M1J+Rr&LEMLd)xNe9dMB!;Sc2V1`di4;a^pj;_%Ay z7GDd?;a@-xME3nFv#)sye*NZ=R=E^MY|s2rH^N{~=(4tU0R40h4RxtfAB zlx`hvvP@Fz04reqzM&>%NdHC06*Qu7h_d9H5_`mTX!&h-wIhjSMq(0bXrF}4A5W8p^_uisY^ zzhJr%g3y^><5vG^oz&Om^^j-PK-oxAtCtW*dRsI0QsAMQsy$$9Dcgs&X?wkJmpAdH+i1x_3Q*|)^*98PI_l6{((BKm5qHvpkq7m{Ix80A9Ly-z%1 zjzak-d}VFtMG~ShT&-p8(p`btEeA7KCVR0AcQ!TvT0yVdW~iSY44>WmgXHf<;E+chvWs z>!L&eu(S<=g2&*OTrJWSAI`wKv+t7WGJccR0|9w=HQ&EgUY0iY+l8NFE$cYBU>^9U z>}9AsrFk=%>_&F>@`W_P-i=6JnI~sU81Q_=FvP>|ZZo{WfVHa!6oS=Gtrw7n_7wd7 zi{R<|9;tV&$83Z96g8@^(fNxHq0hn&jz8gM-pVVac-o_w9Nyk;p}Y^G=)4fPy2Lb@ zMuFNFlUo#p06 zc#zcV?nVf?@3=63j(b_%#Ep|Mt=r&u&a@w%=ztK^3dwmcFyyRpn{hwftu;8>(B_MG zlN--Xw4)9i;PdH%c*X-3>d14jnQTQJ(vu-%UXz!C?_@ zj{zrdQR`YnU+pfw(TiwIx|cL^bgXjYUBA%SHLtG3b(JyD(AKR;|Hn1oR6-c$JY6?# z_eZs=w}#dJ36bEuV5OcqJR@iCnu|9}gb7uUm2!mziQ$ zx;tZU6rb9lXk4PA?xs;cMB&khdmJynJN$U|Nyf&ZE0Lg#9t{w&ueztgBCLXy(?MN&6S^jXuw{vuW7i(ZrhAo^Gqybz;{0 z^l_ECxvL-Up4lkg3i;lqTb*&EOQX}k55o5VC5Ssrw-gRDp%Gt&?Xs2GJ#8aMV*v4@Sz)XnWjhHJow%j52lA9X$=0u-PDvTzCPdhzN66FDM6D)?NGIIr6ynSq zLM`AUR=;I&G0}F4gAL>lIQNQ*q!}4~O~tshKYPU_T~^h>Ak)mLzo01utp%0+kfqj| zup_u5@2Xen_S0v;Z=NTexoH>Q>;&XUv377<6NLkq+BWqEQ9q9|#uH*x*oh*K@3K!q zmo&CU5_E<7fwoqGv5P(-3fzf7flepj6S~!$RP{aY7oh9An24QGD_7cKij^PQbr+^HtJLeFhT)Or3zm z$<#$7Zm{@!6gS#mLnZYy_BJET^p(!=<$()krdLLspZJ6DKgN4v2JRf2Tdck;KE8YG;NtQ78$;;V zMQxoYu%`5e<{G1zP|B^8-Km#qzlpW$zh1Vpj0M;`mfr>VECc z?`cLMf7*t*Qt<9s+~0P-MQQ*S?vf&jD1}bpPTAMGCnSn)&UfJ(PePJcAh%far5s6xtRI9fdwnqHxx7| zbiiHwk4nNaV%oZugRiRHf8IN$^=gv{f)z`fRS@8DIRz!S*jpkdi3lIUi`+YSD@3N5 z&s|RJ;K9cfq<%^?6cJ6^vDh<9I~aZA1(Q-fkSk^W2$UW@uJk9PpxQs6KPYCtP=>aKwWy{wUh0iZIkUvr4_*Q)JNnfLDW{%TV zK(q?R;+Q$MOVtV*Zg?c#ge!d$1JEqsB~1=!77o4BeuN*Kx9YO50>R&2sN8n~BuJb3DG(1leG59x0YNN(9jFnM? zd>k?xT+EDIaU#+c?@C$1fNV>*3W?wCyIu+N+m3(=Q~Ca*tUwPbe@@Mn4TYE|&fYJF zJy|vYqI#txbh-P7&?mJ=esR6n2AC;dBegG_Ayp2ub%%|nC@#SKhdnEoaxw8JdZ zrjh0dF?@%yXs<#rs`a_E+WTqLafSf9CBG#MnzsD7DO64T(ASL=YI-1Y8*%|ShwI{L zkr!#cX4z6FYAPk_&I&zSpz;997?lph|M=?v%rL0KnTZ*S2&#$?JM6t(sB*B+{xb*8 z_7|7q62a1dU!UKvK4Zofqg8aRRQh1mcQC(ggkZTjz;iK%Gc8h0T)ELQ`OQA$*$_Fr ziX!;?RK${^5rZ^1zfdZ2AP=3x1&j9#{FGMDqzGQf0?{BUm| zLcq|1Ta6{u3hnl0pAeP2&^?P~Dy|KyLJ7odi#ByZ?(3A!7-Xo6$%=I zaxM(Eya4I`b#5Zo>n{Aq7_UeP$`6*1|CsFkAC16NX8)ZX{04dP#lvBQVk!FGVT7O! zOqM_i2<<%Bph2`#w1*T%&M9iTo3HP}M#m^(42}z=)}jEYJqA|fko_NDW!rD}>c#AO z_^EajeWr{8;OVJ9U<Mg?KMGJhP+u$LZg0(f6MZ+*A{z=r zW$Fgz06kNZnjJ-4Bl~1c2IZ=By}y4>cYyZR(1HEq*#ZkPE^p}uK;_vnnuV(H&j_Pn zVBO(%(H6*If4e9%YXPrVzCL;o@WH4)bK(4hQDxS``A%LNvA0Q^#&=Rp3pcbr=CY;c zhAXhZGFf(&Et21kaLyD#7EUx9FF;MfhVHm@36N0;uy#|=Wl^Xp__ah^-#3JzJM2Hk zyW5VS%C;JQB8v+xmVPCqCsZuG_3Q94LIUg0_KP^I-tEu5cJKHzTT{R18^I5(c3Ea` z3SF6r5qBfCRoa*>;C;Bc_=d`Q-g_fXID{$0>?vR=LvP8%SkQ{-UAx)pSx{L_m3YBuZ!j`aGE))8GWbXV&D-Qcg>WwRQRO+|BO*P{AuB4v{N)aD{(DUXl;kPTj9 zq!rFA2fRJ^|fk4r3f=@?9)@>2i$ z1?07RpTZ9VkZI>VG8v5+If==9fc`@~jxqq{&hc$tx>%>3LuaN-uTH~QF-&cIJaCl z-^}s*^Uk}guSO;;%ZyDpTc!vS6o1@#TT@_EbO_-4n5kNYUSEw z`Y1daCs{RH1@;p6U^o~463~-vEUu!x(h$)dea%Ji48YoexHHo%&b8?Ab#ayb9J-9! z8Haq``*0-%^ZRUd!G$q%8Tuv8{71k4-7YLCg0C?Nzl!Dbn$A9w3SA@6>CJ2%X@x}V ztCZ=_CJqwrr&k!N%wLGODEEVpX#Sg&V!vb_F(SV&qxMSLFC%+7GoQyQr z6wM9v5{w&ZNkYE4JR48;HL#9aeU2_-$oH$z%HM#Wkc~QB>8e-8gpcy^*TL9puttI9 zSTI5dEjOFGZ{RpWLsBmVQL4VcnefEn(GFsHVPrslUj8p_`1&> z!(4*cnnbYJ$LMP=H#3(}=8F{ZM!X>(r6I(uQqpEE=Rcb=K5(9bZr2*sfUX$yqj(wt zD{as?tojW2cE&O!b~@)VGd_LUK;^Gh**6B+et2AT&5T9=!Y$XCNByyB9wu{7BE9cE zD)Z~Fyz{6$<$5j2q~e|W^7O8mSLpnYAcpJMnNCT&*vy!*=1Hu5na!i0Kz&LfwWCT` zOSTv;8BBM6L{!hpkR|P+7FF4ZEb(S#620BdZOu=K1^A%w^xf*1vR8WJuD$|^Wk!Wj z;X?!;p1bGeF-)_iR8NN-l8a1GWiHvSuciWZG*mXQSX|jkBMn#bypH#c>xVI=w7HJk*?w<2EDEEus&1t;eP&A2Xc6me-P`oNo z{FOt3ffOVsU1L%xKi-np8u3ZS$!a=zV(RP+d;UyE+`dD}XyT9Un)dRukSdD@L4zMi z0bhJ3P9|WXT7UT)g?--3;Q*c@5+3vy;wvI|lw29&C^0@WJiR@P(4WA6ij& zEhK`nj)YcMWSgJ$>VI8n4A=`S(Oscvwz=YV>Zdy!f}Yp#-98^*9{5CKjGg&AQQIaS z8R+7^v(1!57RXa9pob~X>W$bDZ8mOK;iY@0`WYwo2EtO9lF;i_#R>96!=l737KYp^ z!eMQw#bdn23M!GmTu!r>sGS?-_T{ImB5Th^=+39wuuK%|o0E=rzvr2&{2u-(m4YNr z2&-J9KGJ!%Wd6cpRfbLWz0E3K-s-f%#NE}$em_k4Xu1jr>9rFUr!p@AfdFqOw>S>6 zGB`BEDi#b_!ruN46PUXRh*rx_vs*^qR={8T(Ea2gLN$M;v%3UhQ7^<=b%#^DQ`OKGz=(S)7}=sc&;;yg>`Z*>_rKu zY(!{*yBW=(+7p@1>$sG&lRssgjpnk}22b@TKF{?ybQS)&dfI%lO5Lc&&{Xx;8`kFf=?ezV{;w5%7wbF9lkExZXUttkAlBV zko5?qE(`T>WWR@<#fp>NQmgNrtLpQKA3{i3NLrgvRLc_I=e)ugUPe`G9Ij7 z#f&@#Z!K7`_S=%k3GcG5czUU*-2KZh@s_b205Pk~?yVk}l53{_@crngEy{UlCuADs z%FcZd6q2xoztnC6A5SjZrcFO1?MjocRPjmh3FlU}{<(&oapzSt(0fWSbaZ*UgKi5S zfX}4{u9Z*IHC#QlT4C*nl%*ATm)FY*vUa?{(jhrr&^x+&nijP9hND!pfb$k^&2P_5 zf^p?AdesG~HXAb$Ivp!VD$-KX!HOW$ol?eirWGx21tlEGA=6zBcWTPjChXL%3QSu+ z4*hN#{l0i?W^r5vGBqcFOn04ly(T>^-f-Ec?#LdgHe)fNSB_~Yi&zuzrKpf)Bm_vj zXWJHZZJPc|7ikt3cYa#wW!G%x)9i5+!p1Wv)~7)()Po|x-6KG!?uaH#!#YncKYbs- zM@r*JjX%q`soKor^`X(Mnc$i5FQ=0u46LbY>z8Eg)HJ&J+&{CZx|CB!!0mX*B7U#= z?A?)|Bb6WRPV6@Xoto}>$f1PR5WbF2YvvwKq?gAwK5htp4y8t_bZX-JDRQPIapiEl z^D$njii4!K$1>_F8qjnkP$2KTyJNM7YUDY^wuu))#+B7B3FJx6OpbH<3dFDeFeyYC zc}ytph_IrpnLK8^(tC(E=3s9v&bQyRkR_(})S}Mb%pvjk-&M~s7S0z|KHp70M(!gI zEm$*y{Wb;QhZfGN0lRk|g1)(WR@#z51pBHlNd`;?F z0|L4nM}9q`nX+$PnsHNt0+tH2xzl$r%`+}4B%C6*#W{+hIi^>ERZF)w+>>pk%*vD) ziEE`GXaf5WW)~+C_m`Lw777)0d&=?Ws?y-b=dw7Jm;XLbGmRi8Ps_i9mbmqwm2{Ej zGe{N>GP7%h`-l~r1@b$)A_K2neY4`voPz&c8V#Y#Am+Z){qscPlKzoD;9R+gyH78wt~FJ>P=lWQV=r5zxif94R>#l zU30djC9bGwBcO`X1C6?;0V{NNd$Q}=$J`;LQ_nxN5}85i_1m74YB1L)OWl_SbST-= zViU-O6mt1oL323h@eBMmQx%5_MqwAK)_CD{tW9k~ES4*n;}aoG<^4AM=ub^}>x!>j zQ}xL{g*!`>u>_w{8_QS-;lDnfRExJbeScg=O*l4MoML>7AY;yGaCiOQGYYXvt{{E- zE7^H68T%=gT97?wF`+i|mE4+FY!=2#EwGyYL+Ebae@2mYUDyGLP%kNLJ)&Wv2AA3f zNeHN*ukJ$iGPi5i`dEt~77cc%?bIe#v%!@;t^Xw^h9`JmB#~PyA_V?HDLzRUFs;J> zb1KlP`kfUG>HpvO(Er)S z>!iH_hmdj$>#Uw5SjxFkm8Vk;LdU9pt5AEg<(>P#QIr>5{w|F{Adg~L2a zhj#ijS-E(AtMiu~ctQC$K`j8k+91+Mzmi3GcS-sg@kO6c!#fpbl*8yifN;Ej-6Lw+ z&3zzX9kmhq%+d8i=+Ti&{FN-msYB~3iT^%zn-@~&(K9O+KJ1IlXn#EEA6FUtBI=ZI$rNS!^FwkU z*ur?i+8{Z=z?J)~lL9(=>t0Gdn?wB8-~p^eszs+zKwB-VvwtYJj{=jkVPMNjDMkwF zz}BSt$#Q7&^jt3wnBng+Wh~@JD;M0{;BUwe-#3dkoOXB%%bF+PButIno?}%zazHasggh!{DMI|s%#nq@N8dVb?6xajA{zFrS-_#HTR|Dc?Izc(!Iz7rZ_;YyMcRM(JeoOpVev=G z^|3wlhCt=vBYx0@vQv@VH3oRNmc_{XdgR-p8J{SA;N2<->a6;&M|>CE;Uxqwwz(bqcqrFC|heof9*uwQ`Gdz!3OHJ$z_nB0{IRyLI&#Pgq);?!O6p-Er5F=kM4DM&%E zo9B?8`VJWJ-KK^NQ|Ss)&uipBI)pBjEsZ`HN1wz35URGRa;x;7b)kx7?cMZCbGzs% z)=xj;xkN~$#P>kPzh*Cz@z*R<6rJ?^lAd)*79_6=pAhd4^CZ|b zeNk46se6~djw9O7y2ujAThf)Wm#LyM@?(!_b1W$ku_O7CipL4ai+J-DiDL}Ab74`K z?=1~Bm-(X$s#Eu`G)()4TNd9`of0^N>3P1%+B#L~5Qk!G9fmRs=v>_m6M0r1xl+9_ zVq0DEn@5G^T-;Y^+)Dh$u!OhR_J{7*u%PE>6Al#t)M~Y3|AjBgs`rP!0BXn|gbmcbd zq;?B0-^Ww#Bf;z`vI>^@TLY!1lNNVx1)M{H+sJuUbIf1gRB5t40J6TH<5-n0H3C5He-ED>xiH{=E1>S)6p#?Y zrQzZj>N~#o>(k=T_OGov%Y&ipz>4q#8!Mx^dXR>d^?+*3p4C5wG-S%;77~o zip}6qI@XA~;NniAVg2WXQdWR0Mk5EUItGTIHYw?d=N%dvZ_R;sOkq zp^(59n}b0*zPpCmwA&7T==lC_r+ODC&}NNsar&gAoBw&mW?)!*boqZgHW|Jke(F?3 zWPlY4LL(0I#rG~PnT>{|S_=VeMGOtnz>qem7PTAhP5l_@6}7?lY(85PjA;*k^iP2c zpym`8Cc9Vp!i_3W6jmUKkY!3yOPbFO7vi4YKB)^#E%2@Dyr5jk7$|dZo>wHNsArXR z+8q1=8fhr9xiKptC)Q(>EncD=JPJde)OvS?+qNEz#lv~vym_;(Sxe1+}3AJ%$W z^86lY>(p8nkv$KvS8N~R^y>&BnHuohE+8fkHnTL&*i1a$$-fNJ`%6LuOFh$+)TZNc zj_wTV#H#)Cf4R^Vh*kWYz(2>Opf)jOY%5k3-U@e@wfp`k|CaiV{I55gPxXp5)m}SU zE~!6A8T!nu{-962-_wg-4HbzmobRUM@_Z|xzQ-mu*x0eO1 zF9n}HU>NWmX&KQapn z8F~iZkZ%6PI566w%C^Tu{%83~$4&i>ZX%yu^#D%->%nM8Afu(~XQmrhz%Rj(;hk*S zV>i}a?{;f^**;d4p5Jf16@zsAjEV27lojGsc%SWi6~z;qQ%u`7=4onmRX~6aouX@X z#4?mV9?yJeQ@{wW3l=6&T{~xM~F5J`q))9yv`h?Ub(&7S80!eom^HCYB zO=WEdj^Qly2a0zj-EfY8{2xiz9o9tib!~u3Q>0h1(G;-IOI|^wDP2%0L3(e}OClg3 z9TY^RM4BX^L_msC0@6W&N);=<5*(@ zFo_zi*}0-Wt5^MS@$i@4I5{NTLH?~&%rop2zagU7(Bw60;-uKa>=WRHbgVNnKAm#= zaaUUu2jE;$3GD!Co~?pE=mC!$l0W|detPH9g+2KpnJ%C8M4flfXtH?UoJqgBUG?#m znITxRDtR=gZ8*cQ?9eidipn!H?6e?X&l>L0AnOeSzTBP2>OIExAR@1Rr+MKKB}A4n z_R_MpATw0BAW0V+YMe(zTwVUvB)M?JZL_8v(Fwjf_nXpia} z%jSLd$V25_<~d16)Uhc)6xmKKl3f)@Kc<(bIB+oDbA`{4BOVWuD3Fn&Dk_Y`d#&8> zEBXZQk35*VNfpNzcj~I2i!dS(wW(v01-U}Z7s#6BhgDedBOAmiRJ@V;vBfV*eYNzZ z{$%54kxn7;*=8O+|*rE>OMR-=T)~fI7K9fVN4SdT9sxD1z{A%h&kggvOj7 z)_RF&N3I^HuK2#MTEFkx_m-c^^u;G4d>s3AzQ%w2zMR^>-7^^#(+hs18~bsd{jy&! zS;bqaxd?vf;b7@LLipmEyUTkRB;F|RVs80U^oC8#}5vmwIr+|yL4$43sQ^{r@qY|P?G z8J4gEI2d03eUZ@X5z6_1)iZ{)fWId6y^M_b%Cn61o_Uzo_p6E2562Tpb(uJw#wrP? zL5WaaEKSm%1z6Tm$QunFW)*7GFE@>{pLZ@Dq6k;4o{-C zY*Lb0&g6vM_h?_^I2lPedNviX8-sEwiJ$FPpwSPJX_b=Er8?m*kcNU-G&OU3E3t37 zGt3_{icWybh4iFS_#V2K_sexZ=&fQGU{{&a)JH}uYC}5I8lxcAoqYhYJx@mf ze)Q8+w?a(9oZv9mXOO(yqM0m#Z59uN?n-R2C12ddxh(;^7Oh+jN4_Z}y*l=UT51);W|JpoJ;IU7jN- zQ?){8%`|tw@RbMi6`U5V0_+__!GK*n-xxkeObT}IOQ*-Pv2T)sPr;0`&4VXle5V)z zp}_<4L)Z@oQSp7NZx})RQ1}U3ls38*#|Lk&-O_8%qI`~6!rMdj;-Tw8^k6O zHmr89zrz)7CF<2pQH$L*vOdt8xWU`oy^T8#5$P0gNVgQo2OC(?p9y6rxs zJgCn~Kp+0DXWSUk)ZWpFVv1_-%7iXo(!yj2zWw%TnK$Vvlk)B-PACZw)IJwC%dl^n z;e1{$B(%d2P#oXZ2jC3pK*Ehd^-sx<*A3l`9kRPd7&w~LO8 zNQ!GuKnyZBKIf5PDcbxy^CNGO7`hUrkU&4$->Q#Tv zoHO3}+aoa{AD6hy50a~_qD(Xe6ilvZcl9iNiA?hEy(n5ty)bQ_&|6^?`-WY zGlI)b_XLaH)M~qV?7Xu(gpqa6`?2$gjvwvl^3PeBE(!3RX<*yPF_5-Ti%t-NipjY->0w=svsOa8z!n2*AjDb>?)^M&hE=KrT@=2|OV1lc{@vfDSZK z)Y^gsnp`DsAM_L!+}E>l@BnTT^lw`w-W0~N( zE@?~3L<;+OE;X&}aF-Rab#B1q61HdVYKDmUu5ga^p58P$v)7FqDkEcv0bkT2Y2)YfB*;O2NgO7R%06j=@ztr>$P`6tK`(` z9187vYBmk4s}d(P7a_U><_B=fa26>=tAz zVtYaw%Ca!DkND=O21gX>Sw3l~f(`+%De<323Fkx@ng!JEpC>vEOdc@qJ~%3I{3Yr~ z33YPA%tVZgKF5BuaxRT!X?XkS>k(YB;>NJaK`oHc?@*63*Bo4)(ye)PvOC~IO0Rxo z+z1kO;9RP%NR*YrEST_VfvUJ-z-Ot7?}fAdk8&@oqJ4}Q9%}xK*sFB`J2ycB+<5@mZ)tDFlDWTN! z%?J{rxFP)b41U2Z9@LuQhd)qn)Rxe`@=IU@sbE0LI;Wt@XmTCkCUyTbq}UI`{AP$3 ze9=NKDHcr?{-yD}frKy@cW!n*`sf{0FRaqz5%Y>|AVJ{%*&|n*o!$QHjg~ZqtQ~z- zwNjdcFb&Q)FmiCd-NZPic~#-7xyf1Vfo=0NSBz?ANJs`naVdL8M_=)Q9_)*EN8Lf~b!?5}o-4BM~< zjG%+`Htd3`6WO29^nr6_c9@?M-efm68?bl*C3FB`vdUx9(;ZYB*y5T*VUhwu=ZPlQu`v3F)fIHVWsb(HxMO0d z-0V31X43(JN}td2S*q}1?xQHli-44`ewt_?g^^c`?+U7Iu^5vcScdnSY#)8%jnO2C zRhWB)uh^(=z8mkcJt|{1;p(eOa@DW73zgwPmSe4wC|?*E_-`v#!@^(qGFmVHO=zUT zXhn~bQLNfydKcsA|1l)gBKp`)nuT|;Hg2$@G3x6Ur!o1aW+k-v!UNB<92K1xD7||u= ze&qJ1XR`4pAGTEhgjc=^r696bYNyM+6Qelyp~vh}?q5EB1WWpIo+LG(bks0Gw~yoh zSQh_sFcOSBsy@(|KccD_f1wjKp#7ZN*EmAK}CR?PrZWW2CyDQ>?hbMi8_FdWZ`iA>Pr~^yn~xNjnWK#K zB87pC1vL}LBi$bGqw>XDiM?lh9*rl^pfvMpWh-*v@TG@=>{fdZUp`}w zT^gn!k$EWyKkeSG`+;qDLLqwewqhB5^w$b6-mkSU?0DcY?%8oQu|M-d;R$IebU@`8 zlM=y94i~us+rH_E5z~J0DQ{L;Wxpa6$hzMj}W`CLh7Sc;*Tysw&_(&2+aq?8WCuuyRL=V@#4o)7Q^k z*1sVpfv@oL)!<{a+&hR9^v)9d*Ji>Io4`2sUib^B-c6Y)H-#K; zdD$5EI7+|Mw~>;0{||M&vGDQzO}u1T*8Fc(XK3^B_8DJ4Sf6B2LoRUKSmV?0M(0M_ z?@_xXcdS(`A~1TVX_ocoTRq&1M#U%Pwx)23=` zHJHjwGWd|y9HB+kZ*fNVNaF`-E)68=2>~3iQk`N-PE^qQ6XTo5IbIqas?dmK+`p+0 z5?oNIo$lcsDXZg?aM*Mw#^-0)UVd8D8``c*rg zDxaahc^}<~O%k1V#F0-L7)!Wsm~U~{IP(=?*ty?-Yo=o>gUa<%VnCZHqI(hJkC$RI z?WZ6Gcivrvd|PJ`QWh<@Im_I7Lz?s@wUw)DluaR6G5Ob)+W_hqAg+(AU|2XwLDxGb z6;mfasp5~FW49J?UQ8(+j(#N=1P=C%tK8&`F|=7!`y?=O6RW0wyDBJ44~}n@3dRYb zK88Quy?X^20rHzn*Ed zRQXJ@QPgzsC)E+`hQN&8dK(RGT{beCBh%dEw*~NjJe`n>&wwXxwo39LrRsy{K2M`1 z-FKa1`sgo`qhYQyMDs6FO-(*M6QBwkgy2YA;ahW{yJ(EeJ$T=zr=eS{%GbdSM6f08 zm_Fvv?(tTT((fq#S@tayE!2hK7+O)6yVTr_<9W!8KEE(S?(>`$b)Oa5^`D?ym1E&* z^9Q#Jul(BVZ4Ry;A#~1U_B4~`kFf)e3|)T}%j@js`Ip@s<#qiumUMa?Cj?EcvPs(AUSz#UK3 zo_hICZTVdRp}*pU`({`Nr&=FwbV9>E*~bQ|{*100qeTu3>U$y=4pncrkVB^GJ&ayA z+lv1jz36clfGeddZ9M+Nq_krB(d*m36ql~%3~IAj80Skxggevt1rkz96VNilTT!md zc5)DU4`Y~0C*nfvt2EqeV>WD2`+l<{zpwPa!rUmB?0nVGR2SyZgAA*h?s&0{G<5g4 zY|xjWk6>-ZuSSb8=Jv7C`JT7Iwu_*iY4U-tLZvW(hXnXhD8K^^`iCx2MWNun>G~VJ z7^A_B%`Zd#vO=M2UC3?m-^Zb9V#0S8ooe!NK2t)DCUOB@uLpq1z7ZvY<}xl3K|C)jKXXdeRv8i04@* zaxTYNsYnI!KVRxKK~(lk`6rVgsxjAy|1%<$xRaX65LNfG>w=huL~ECn8oU{1<%tSmlY+ zFmHI&R*gkWzpFL&j~RQ8{XF;+Kv}<D_Mz_|QdQgtZW24?sAwbOt+O8zCnV+vrBbg)Nc|86ad3XwUgz#?sKA!ALpJ z!PrUVsom++VtD7e!xg|WL~L#FK}iIZI~;W=|K5PTP5FcZhm8LeTT%H`^hy#h|jIWFJ#qnyNP&amLic$*NAPzsP))3(8GSQ zF5-pXLkY_;_WNBIgBb-Gl>owKHv7pn$0w z_Fo%vSqAdA6F4Tm$c!~7PM)g7hYJaBJ8^e=EY^UyXW#|*YzRWoFi=8#j~VM+E`7Wb z4-vNgchI|L;^eZsDBwJdoxj_uKZB5|dSDNJX~tR?M;iKi^z+x1=zrDq21uJFm?BwO4GF$a6@w;0#h z5%%)+t8^;k{*VTw3&FbyVTis|eY+O(AyDt6*_N*~GvSZjaEpYZ{JyX;M((SGBZ4E$ zW^W!$;U|ZQ0H74;K_%)6^?Hgl`J{-(w=zexLpMh_nWK;_ul%h6#1Enh8;8q&FFQF% z`F7Kc>m;K~AO3O(s;7J5{|uS`l|zxlZ1Z&+Dcu4d zu&xOD<& zA^c6pTWe!}FG(6iN^pl)n;+AW!DrR+4<*Cx+-xtYEsNXN#(k{$q`V5I52*#w22W!C zSW9t}KxaBHv(K1}zXmiKD@5LdmsVKqUpD$1s2I3D2 zJQV)?V5if|MQsa`>)?ljZPesFDR&hZqN_1E4d%u_T~f7dMUgqlHNqRR7RiG_1^kX7 z!435Mv4%wO2>;o4f;R5`>VThF5Blx9R821Zfee@|sNP`n^b|CP>;b(=T z(ubau1@5wC8g&at6cq}uq8v?)<^3b%vHfdUVy1G?*eF)e3{_N^A~=O=M?EkWx@Ekt zC7K`Oxko%=O1k?S!8bm7N+2IATZQxHK(l#~L?I-p1&lgpx*wl~qs;;)kYE2Zs*G

r0GSrsn%3dR?`LO_hlEGijj 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�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Ⓢ#d7?q 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 + + SSMonoIOLibrary + SSMonoIOLibrary + SSMonoIOLibrary + 1.007.0017 + SIMPL# Plugin + 5 + 5 + + + + 4/6/2016 7:49:24 AM + 1.0.0.14081 + + \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/SimplSharpData.dat b/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/SimplSharpData.dat new file mode 100644 index 0000000000000000000000000000000000000000..816bfe12a1cc8843e7a36cb83b974bc9598bbd95 GIT binary patch literal 2016 zcmV<62Os!Ab6Iat?7tDe_glwHG}?u}Ox7g!_W1$4ugh(PzP;JNl+_f0#EM?6aXVfy zn>qV?Q9T`KA@j5FR-I)H0mEmY%ipp7Oq129Ei=<>MXsQmfb9uUVS|f@^`^!+)L(tE z-iex0=JbsvCU*Ks_ZxJI!R;n4us9K^kugL*$nZ<=!J_bFQp^l$B6g&zH4OSrUv3>H z_1P+Il{=->z1XC4Tk_CU+4NIuD-QdJ@4UX`KhNtrNKE<1v;7`IZ0S0>$()0Mpj%L3 zWI;6sP>t=;>G5r+F$<}Cfj}4T<#NIyE2u1KQhs9C)wB{}by@7J zJRM<|i8KQ=M*^j#4fDZV*12$N%iP#rB$9OU9I{Wi3?N?_qt}AWNU=hszjg{F^sDOo za-L+}@1 zdE1I0#WO~qCGvpYQ87Eef8}J>9Fo^w^PG~4h@w*C|6YDb-goWqD5@jAii*ErPyUGx zZzN4+>(k>4xDO6)Uc{w-@x+0_c7@*!>;QH{9})*!q%eNyvPP(_xdK|1ArySVE0!*( z@#2C$BxQxzAGTojKTMOahesuz>A*(nn08P+j?9d14fCIo5?r-K`w4 zL8wJGY>71FS>NyBPtxQ= zKJ+iA@pSVjRTLr2eu&$k+2E&jHOl^YkXvFDbG3j32k1UN>AaB~_mkXKO1(%4^}*&C z78!R7n6R+&;mFl;NA$#$z>16`0yD*do^f{@06Agk1;|eJn-|i3zyfrZmduVP1#P1d zZ@7|S@$N$=kqO-VOEmMbG|)1V4&l&?raN)wXGU{mJfw+%Ooa9aD6~?YUsx83IKC~i z_z+63S-2UT0TPm)A$raWkm-W9oi3eheeP#?7-z&2AmYLg1T%*Wsi|m#cyR`_rQIxZgi4XrIj5u!VviuS>!lFJBdy{jG*Ij z*}YjKpicfy)e7n$w2ui#PBLhp%4E@VU9)jqC;CFo8VP+E%}q-uLbq%7o_RJ2swU&? zp)Rl7!2J?U%a^Ntk$yJ}pw{bBBH~ut1{ylPBUvUTTCDzjv8S@)sWtS*Z`};f$M)yT znAzajtOp+x#$ilxNNB9y(AT3S<*R~jeybYOM*9C`OCgdbFR)w~?cVQx0Ik-^KMTar zwiZ(1g0Z_1Q0V&1U-l(}pHRaicB$5Q^U)kA?x&v-Bp7W4@5wtf+Gh);hF(o08Bph< zCSvem!XG@&SAkC<1>?%hNuO{LVgzo-abf zu-+*FUl5ItHC>jW{U$yR2(E}4I{oX#K^oVBxN5URk>8r@J>b!vo$=weNTCbZ`7#+; zY_dn)AV!A-7S2Cu>JjkCz0t)6q77ZfNEG}qbi}?h_}SO zTWWM|g@5Oeip71djRRGgHK$HPO{#4OMGlB3ZJm$l%XKe=sUm=Nv-4-r#t~)7WH%SF zZr3RxqHXR2EoiC(K)|OrQ1m5&Abv0i+_*#0LP2!&+6v*_%lI*J==ga{!!CKH$=ZsL7fh8F&JN!%n*6TNQ*STV(tr8bgQ{)Vei_%u*$x*)@bc8!gKRwlq^RxawFMgAwnop?o}8bPlcBthPonjQ zran2VFyHQbW%uf^Ma|Igkd(iJm7>U)z5c2>=A^zwwVfz0mV?Rz+X!jC4*g70<^(xA zBUJy2$!Ml<5Imb^|D71fIWGK_gp0$L@5v%7ze4vYkzMf1Ac|!9WYFEn_caHBUs_eufr=T)IH` y;wdgdaLtpd5tb%1AV|#v*mrh(EJP+kAPh>F>f$)0%$BB@v%&z~0>1`IB7?)Vu;x4f literal 0 HcmV?d00001 diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/manifest.info b/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/manifest.info new file mode 100644 index 00000000..99eb2339 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/manifest.info @@ -0,0 +1,18 @@ +MainAssembly=SSMonoIOLibrary.dll:6c69af117dca3f74ebca99f7a0e3181c +MainAssemblyMinFirmwareVersion=1.007.0017 +ü +DependencySource=SimplSharpCustomAttributesInterface.dll:9c4b4d4c519b655af90016edca2d66b9 +DependencyPath=SSMonoIOLibrary.clz:SimplSharpCustomAttributesInterface.dll +DependencyMainAssembly=SimplSharpCustomAttributesInterface.dll:9c4b4d4c519b655af90016edca2d66b9 +ü +DependencySource=SimplSharpHelperInterface.dll:aed72eb0e19559a3f56708be76445dcd +DependencyPath=SSMonoIOLibrary.clz:SimplSharpHelperInterface.dll +DependencyMainAssembly=SimplSharpHelperInterface.dll:aed72eb0e19559a3f56708be76445dcd +ü +DependencySource=SimplSharpReflectionInterface.dll:e3ff8edbba84ccd7155b9984e67488b2 +DependencyPath=SSMonoIOLibrary.clz:SimplSharpReflectionInterface.dll +DependencyMainAssembly=SimplSharpReflectionInterface.dll:e3ff8edbba84ccd7155b9984e67488b2 +ü +DependencySource=SSharpCrestronExtensionsLibrary.dll:655a49edee523f150d1c03bcb5db87d0 +DependencyPath=SSMonoIOLibrary.clz:SSharpCrestronExtensionsLibrary.dll +DependencyMainAssembly=SSharpCrestronExtensionsLibrary.dll:655a49edee523f150d1c03bcb5db87d0 diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/manifest.ser b/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoIOLibrary.clz/manifest.ser new file mode 100644 index 0000000000000000000000000000000000000000..ac2eb253aa15d56f188d7856a8a1241e1aec75fd GIT binary patch literal 683 zcmV;c0#yAUiwFP!000040PVd8U>r%7B_=Oz%6orG^-9;^bfE}=m|e(^`YTQU3S(MP)gzjJ{8z9^Q(V!e1) z)QhXE&gJ`^2lW5l2l)Ew{cmT@oId_^`su^$e0TT3Y?|FWf3TlVrrE=X=MS=-{NC=t z?vxt8uGaN*82gu5J0JY=c{R;SZE^DG_x*ie*40nv)3*72f8UcjZ`!(A{=UEO>teAg ze>{~Ji=4jez>0h@Dd@#ld6{QT-uqf<^E6-O%UQmh63mYteb3PKUWcye{q;PnS10SH ztro{^TNjgcn>T06Hm}dKX|C`b(H>6sC;PMg>HXb@lTYv8&(0qbT|doMHM;CRrc$&3YFilT?FP*DfLG4ig`=gIuQ_$bL*tep6 zytl7M@i?qo)A{}oonPhWCCMbIkzrcr_s-7`^4VmP9qdo1vj@BP?@t~+JlM}aeXxIU zFxiXKdKhw--nW2%D|!$2`1NQ$4DZ%-|Fo;eS$ed?(|2vYga&Uy-Q_80lKlOL`7F=x z@7+7!y}vWto$lP5OegnelY + + SSMonoProTaskLibrary + SSMonoProTaskLibrary + SSMonoProTaskLibrary + 1.009.0029 + SIMPL# Plugin + 5 + 5 + + + + 4/6/2016 7:55:41 AM + 1.0.0.14269 + + \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/SSMonoProTaskLibrary.cplz b/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/SSMonoProTaskLibrary.cplz new file mode 100644 index 0000000000000000000000000000000000000000..927a567e1c8553fe606c0f5cbeda4b77fc83942b GIT binary patch literal 195425 zcmY(pV|ZrKvMw5Q+;KWa$F^S-E5NMDmzCRLCfcEGyP!JF$C=d{M5E2kuLpw_|Q)d93rJb2QEXePF zKoAg65KIt`yQuP%*g4SJt^Z>Y%-Z?d^=Cn=lcuaT1kgf&kiAlAepB%7ikm-RS%awn z{LUwGv{bP}5+yGB)gg3m0zM(*`Ec#g)fDc8w!zb{)9gaMz2BQ{RNZG>>#uKhR1>m^ z8KIjz9oG-<tM@CtVt$kM?PslfX!MlBW2GtH-TYmXDT?p23GR>Ik+yQ$5G$2zeRfqUMxt zw>3jBVQZS9>LO(FtDcN--*(@w9<1xCd$;6Lq8sJlbwM9qKjrf;WOQ>g0l?-`0NP5l zd_IMYfzYphNYXaNvOnMghlCxw;EB#ZWmdA?9DXe_qsR1JknQoAOMP~fe@pR60w+a( z8uT+y(1L(@fFeueKSA`-Ak$lR?e0tUVE}%vCPE0qwn~i1Sa!$y>v`cQ{(w`8;Mf?2 zlOiwDfqD;2b=ql>y_Ww{{vAE5BhKh;^~ajJ`}Z(xoB`bKunqjTn@pAOuf>q~*bNyv zm9qto_$&vKFem}irZ_Sw=pVSjkkgY@`g?0B;rX>cI$2zHmRaOb$YvovLMi;eya4$xFME+!>Ha^egF9P}9Juq%uVZEcYV$lTwLX?Z=wC5P!0+o# zz)$3RC*G^g_ULLKjI6Ah zK6=j%ef;MAdOQ2HH=oMl>*4QA-P%~&+F5JweLpjCt?Ojx-0A*4D_wx{S~xqzP3x*z zetIkk=$c%Ag_q4yU#MDiC6D#p*-6#<1n}8r?f`+j%(rmG>jcZ&b;x zky$;lf_QHLC8RWUk92cceEY&8uZ-IJBN~0V^2k_xpPAV&z0hZY({u5A~pnDt#ALsj3TI|o;yY4iU&cfbVC!2oY zR;B|Po)13H3!tU3_1nOeVPj|G6X4e&AH|b>Jw4sHX42wJpZ8sD;AXyy&b!G2-g^GZ zJ4A|<99?HWmq#to+W)lt;Kg6ECRcF;&|P_L;4POr%Il}hximRhSQ`uqP0SA33JOIm zMSK|B9()LW{dzRG-`yK@h_Hw0;htzcJw$Kxo|x{v7TNjn1)S%(|Yl3?sab8 zXEdv|4fdqX+!uri-lbQorn-eN1^f&J@@;kYdVPDxvRssQ0uirepYK1jp%8C)dE9Ru zfP#KKg&>^^UT}4-S=$zF1WcKPeJ?{I+}%$+xI@Q#Q(e=In)O|n8R0w|6ZhTTZqE)K zy>5Ot+odN!{OSA9PDJ1}|K`>!%RT{u|7AN9J9y8L&QHJ#VdESG=ZA+)4kS{?lMHXv z^rENgv2bHk>Q?6E4Id9bf2kh<{wDYD9I}}0cE1~MDg8;J#zm-=?0jvI?yH|q-0yj7 zcYVYBk<(HH;hAPdz<&ByKac?dIhGxG5Rm_y-}Io~Vt*UVa(Dlo`Tr4=fBCI!Y3pF4 zY+>l+AYuqGq%$!D{O>F;zu~ngvzP@PQyl*9q5nTtz**B?;j$OG7pOfV&9WZSBeP8M z{QL^qRXbrD($l@xm$ZZyFchIz?Z~ain92F>rO1nCLHN*ktCnpF4?1X`H?dRyC6l;R zM$5clBU+P}>3;>IXc!b3`c(3lV^QCyemguv>G(04l+5)_5{Ltc=)WSPtK&c}k7pF& z8@-jj=`Xl7QJVZ|MdVsk!TR$}PTvlX>}ie4HkrG4v3s@1S^IugW$jVfni}Cf?53;d zkbmZaOG4&#r12A9$mWczV=OBuAWvJt&_s|GQX%?k?(ELClo77nD?k9_`p9X3kh+Y< zQpwk7b*Tx(&_&bV^2UX-ikaHft++pnIk3Yd>dTSpwq17SMbYY;yrIE>1vY)|uL{X* zt`5%-RXj04GVBZl^hB0S8bo-KLbWm3Vjeuh)NmGX7ICoRV)%!Co#l21>xs=(J(2{# zJ#K@1+t1(nScNNrlM?kpMZK=DB#)Kn?@l?UmCA!}fj%>~<*xZJxt-+L9l^zx_+q2d#3i0B6T?F++0WyJY&v7J-qjI~$)cI@+ zV*GA4tcF%rC`b+-a2X?Aa*n|K_lrlg7LSdH-^0vexup00+lq|by?#d~%eVb$rww9d)$L$f$%JU$1Jo&WWq135d?cnJs{}0u&-^lpwV~pqIcI%h=V`7A zJTVJtiFuwcdP=aqK^{hEj-1OOQd`xy^&$|r98H=SW*#2V(nUgAt^k>{;Fbqu2U1%y zI+#2)UD67lhGLLFl13FgqjV(-^SpJPzatR4s5Fr(d9$#=U6-}W(I4TXivSQ*~5+tL78C)t*%eMcIV29BS^|?8(m`3iCbEm)-#$C{)o1Cw;><^44}sU0BDz+~c&3 z(43q7e?Xn`@tt+W;{p>m)ug&5V4nJqG10Ny;L_^q?)OHQoWvi8lKLW|h{2eL19BYQ za6mW>Pa#I-o-;A#efq!vDJhdNsj5ga|V4KGMiwVG{MS)eEhIfS0maB*jWE!lM0Ha^$p z?wIC7sK56I5PvX-{wy!I2y%BtLf87hA4779y~6*=CH80i>m=HFX()BgT?kq95dzrf zQ)>rEP%BQxV;w+xMb|v!5ate-`NB0G^<=@akE{xfg+LWyW!Z37aU ztA|*Vj8v=Y%e%g`VZWU9@vr^X&zX_ulZmvo-L)zx0@S~TGL8}!Rok;Gg``K7fp)%? z*b8EB-zL(831swjI+$16H@+a%%VYd-L$fXDN_&CzoyZEOZ|_F5$J;Z|RnurvdMJou{^F zy@TsMKLg0)&t%XNY2^K_G)xPr@;Ad;hq|?`0Jb51Cvg$OJ~h$cs_Cqya>B9|w$P#o zVdS>iv1b!5^ugss{w|FV=Cgm1O~*{wG3xDBCK$O4$`2NdtfTGC*&( z3C{@$0vK!(GY|iZR6)XE8tb5bwEa5LS6WXEFx=!F0-$llm$I+EyBD#pHA5 z`?z=Cs>%=zf67+dOllan?N}s@*2?G_0*2`A{o;C% z4KG+x+0|(?1olpg*vw*1R&HWqepifq!Bc1nA7>@Q&b5~*@VriRYW6lRsW&LOU^K1! ztAg{ms7JIpn}R+iXdHY4+Oh}XQ%33d2PZeN%6G(=MTr9vPo~*-Hs&ZN-A8h8;9XJYgCAY=m%<{n4%&-swioWzJf82 zQ32!jXukW7xJa~Q)Nv1L$dT7Ku;MN4EPX3G+uo0bS9j~PVq39RulwI_TzjR83gxdt z<9F9$e5HGK(Wj5rz?iq?(2FJTvlBN@3a8r#ZlAlySKT!TxQ7Sy9Dv#LVd69wbuYzx zvT`FFMs9MppPK8Np}5)1WUkZsYDdQ;1^mG+mJ}Xtc!e#29p`%Ar+_B+l8BO&Z5Tyz zLIbL=aBt&_spLh&J#)zpqdDvF{ynKJ`X1{Mw-o%Y-}V~sojz?puMEtlBjZkVD$zNq zjybKWbZJnj$YM-)Uif>eOXjp6Wk=3YCw}MYB5CBQ_qb;?hC_3NfVaoxKurkwg1sdU zZ(QC&iNtO@<)i)OfO2n2DU@|`Q@UZd!sDiI$W+!nj)xh#aBQV)@X6ywk`xAyo4eY} z*1}G`Jfer`RPTz-%SF(M%**x8G>7jwv+}lw;a7<|;@4q8IusijK0cHCBSbCF6Hp$p z?f4Vtg!lDisPrHw`+LJL3vhg^Xx@z9Qses4!S4Hb$1N}W-uBeu)9bNO1EupZAQ<~~ ze}vb{=Q_QM8Hh zxDat4($~0nn=nIL1dIM7C%Y;akEM*8iMWDZM-9HAkhaB2-L=Y;#n-n)i%YR5E=R|J z79tN<(gE@+vctQfpk4<&RSNH^glB~!T2=!5&m8v_a!n0kB$u`Agq@5dvtH2!zs*ih z_wWj=^*%T=f=5EeB#~#|xXFR{!7rW^0;%Eb()kM^ z`4eO+m*>AO5w(6IE823uH1M_iP>90DAx@VWg;1~XgqsX43K*<{s(sh8=Ax}cX_@Dk z)C)2;P)OjE-OyO;+41zr40cM5OHyz~$68Oh5I8|ASQqo|#;^>=dH^~SBXR)T;&eYQ zyctE|l?4wPJoSaUh2MoYN?NW~mrz%XoAcQ3*Fea?Z_1uF%6<~7+&{bi2Q07BwmWp8r%FjFyRAFC|QHtrlvodcNyxSgQr2-6B?M0}H% zaKguhSL~WZ@9dIXZ8>y}UC9IwrmYbS0(mm4Dli_$qgz?pPW7gNv8l%ix} z+l1HR5xQHeKz1GxVy7&dw>Ccduu;QD4S~{ne57nP#nh0CT%qm*D=@+(@>--^Ets*7E=_D1d%sClFyxH=1;d8b+?3d z>^bl+e6KPxPqM%k`LePr-9iw&8~Jmn>$wj)nIVd&dgGt+na+hoRC75Esw=0zVeNk1 zSiSbQ|JZE|i-;^k6g7Hfr0ZooEfv)Ml%j)LUuj6WC~HmDr*BK{uBG{F$7cB=4F+_iJtMPPsL6Ngj_fWbO=0 z!C-N1DH-4$8RG$%b+6hcx<}?;y5hr^H=@-r=Qk#(R&?i?t50jT$^P_@oM6oWUvbB+ zD-CAo$Q~P{;M7%cbsHC+Iz~6Xp6suthV~#Uat3)^#}v*YNxcehV8}q*An{%AdM(M9 zm4W|#>hk659dbPS3twC-V;}wwOU&Ss5deBWwOlk<=hTc20ceDfx`d?_z>7kqSVe2i zl>=N3yv*H%UOJAc5H)8skJFY5I$Na+0?ea%$sAulCZJU-ONvu0<%%&j;HP96#kwi4 zv%oh?nNd%z_;ia;v+vxP+sh6yTPPDAg;Zdl&5f3d(>2Z-hOEMxv9vy~-Et5Dr(}oW zvFqzklNXBAQT&Yx*i&Csu3$91D{vjpGim|V zx2`pO-D>xg-Xp_f?mF&Eecu%YM5$9BYuaVl+NvrBXLQ@z zxUi6kPwQr!tFMgXG5Zb#sASKwnSHxYsG|<>K&cQ#kWK&U_}^Oem(3Vw(~KQ0{s*4A+x3kN=SI}kNiZtKbruL1Z`Li>4Pl;wc`Vu+SVW+1 zS8$jjhI+hCTU0c2uOB@ixYry(H}=*B(jY~WrYoyyYxaXL4Utsa_2v;?edf&fJIilc zfn!*@iR9tE;35(}fDz>(uD})W$OL+44>P`8dk#T7gIU)MIE&9+^Wi zkhvODj@~RrR~cpicJjOPd9b$a;>3D>M*EVfsX%vO@2_-94?nSc??+Y{T6R^jf85Sa zp}Zn5%wpoX8?S#{u4YLN$GIfN(gORNmNY0Kf29mBkjo@ z(*Q+>yyWAqGnZZA0WE3F$w;!sYZw=ey=(Rqfl_?i6KBQjWh7Od_;!z-M`8@stP1r4 zpDZzu_I(1ybygAL2VH}dhavo_exksz1dnUpkbG9a z2C36;!H`2&{lmO)Vki{UmqZ*+i(Ucf<1V<+uQ>-r>7Pc-1CS$<)Io;enCO=Ut}qLX zLd0(|`LLnz!KeXs{RWFp*qdCp%OfEIj}x( zISluYda*GC`Ui%{0cLXFSguC( zvv)h!6=%8D)vHwQ2I_ci;=6q#bCz;N$V&NA{)6R`)lAoU)5&AQ)fsDc;p^+t-aFP( z!HjzHP#X>Y0JYuB3d8SfDfzqI^<{exp2jL2V2-`{Q?{Jpi|B(#s{QA|ci{Q$&fRSw zfq}oD4C0-SI0k!o>~AD-8%sDLS?J4qU;ph^N2%UV5Od?_ox?ZK4C$GNM9d%J&l}s2 z!QSdvoqT^hU9W*%a7g!&r&S@I*OfPY;JVJ^D`=UTErFU0Lw2^oZkwu4ik2f#Oa`r_2v^#^l}F~gYE@jAT& znJ7#9r3`virOR}1_ZvB#-Tppa8|K2#lc_LC(1TM^r=e9B&EZrDJtsA$D^7$%WRi^m z#HL+P67(}o#Dvm>pqL|#&5%c&m%h?SO8u4qf>-ErsmdxHW_DwS)b6mc(LLl=d|p&f z=;7a)IWX13m0lNFl^2Z8R(K3&p;F|SOI#QSUjXnR^Q>v2z8CT|H3GsCA8?t4g$`pZ zAy*~p$rrJH0;H_tmP>(tf>fLgX0uvA8< ziBEe*)NftP|F33yfJHho885021Tdz>iFWf`Q0EzQ{CQ&eZrBG9KUtHvljys4l^N-m zcd}3Lf)WFHi?dV2o)!IQBQ$d-Px>4~bw7jC+F{DBNGTvhrB(I_OyiT?#GY$3_B4xR z4jykG`}j-Z2|mS*YCr4DeA@K~fdPPPh7T~9W`k->43oKDK~^aOrQey!zPd6t3H9H$ zz2N4FosigTo+v#J|4^EIJx*t}wKu9ZNf5_4YXHpViH zZLH=7hd)r3B{2@SEt0E;3GUG|Sc_CMFVZtk-0(+z1@kEg)nc8KK{X3}BFfocfFDXT z!A4o<2&zYSa^V^ zObAU-zAfmyn+d%24imsl`DeKU-jpzoDo}vsZ_~XjN~O_G$^+vscCCAh2xUZFzX3EZ zyW&>$!QTkm7BJe3BRXSyL}36Vl#_9(=)GFvZem-qi_NA}^@BbX>d? zh7ODtBLO?PAmWKKpTZ9)yqt&S9WSUHDN;lf<-wQotC||Lip#E6UcQ{%zlPu#ZMmsw zp3OMC`pXq=0fk2ODOHMhG?yUAAFwsUw)Neayl{Wp3;wA}5X7L^QsO3zy=)L^2wHb^ zAs*O2`L@2j12xE;u%ED_u6i1~>SePjhD4ZNi}n{!@Nz5chI_Iuw%wm9ial=EGIDmp z+TR5n8e0aja*j@*vifVE?(lfl$)gnaVn}&S;7_x^Wtia^GN44n*GBR_J)n-kdO$RBbGOExF~u zbyBd^F=67YvYOC>VnQqhVG7n$)stE~6NfnGZqd z&E{Vk^8F_BpN@C|5%U;=*!A`&toxWZuo2+GGXhWjGXzODM>af+f)f`49R_cr)4x~` zdgemLRyR$6gH+lZ4qx>!0!bIY_uc6(BcqImgOs^A1St{>UJsgS3~qD{X#{Sh<@ql} zh>K03509DQ5G**qUoh=9CSU3<0ZcH}7#s@yFQmjs1$}ZjJX#HLpLDQ%Q$Wn%_Zo^d)bh5VDE;>Ad8EImrS`njI^sZg(TfNm zeLtjA5ApcA-VlD;5a!iN_wE7J=oGdcOX5hJkaM@hQw`Y11H9!nt4tjr7^Q^4=*llXqOd zJg$h+MHKak;+rqGktx?n(mA8$huniE zSk4WY+j$l^sY_$E`4>dHz8S4J^mom<8?K>g*p6$JKEj`~)=&>80|OC^js4!}wTj~E zx_p=b-H?y!;LLDg3s*ytamHMD-^*7m>FC|00sleQSXb^`*TUvbtSDJ%;|PfKl+ zf?*FS3>@Bceq>!CC6@Z@{8%L*lkZ<8tW!^^MOY`CCtD?~^l=Sa^xV3?Xu`Ya=Upc> z;=trSRPDthYN@Xm)_d9@TnWc>l8whBqHAJum4MnHGy?p%M5^}`x{7m#y^8B&;!0)k z#KPMktWn`9=e|tT3r%Ladq zFP*DYOF0(M78di~3~hkNZi7x>ovS)dW92=&tslGU*2~VJlvLOCMW>}Mo2I)vRnS=&uM!hrU7uyXVadS;&7IDaY8HVnKn2-)ec)_#Dtjq@v5r0hw5{xJ$@h!8al|u&;aKq! z3rQyjR_d~&Z5SrsGGz4DuOcuX=1QQykrz`>3B^WTk&7K1W+$v z9sqZ7uMn_$t$7RUhZD0_Tp(&wYkm_pAz&R)@2@5Gyj{TV0k(qUKe^nZ^si#>%=qE4 zx9kN7$Imw#YBKi%+66t{X;j+8fINL+$99Tlu_5%2P;xYG)F1))p_uI&GQ{Cad{ZH* z%eV7 z+q-W&R6I4xr*w%g2dX^k6CBa;oimk%HG_o}za0vhw7Ts22k(fF^jlS7yRC-Y93JBZ znV--J?`Mm-bcAtkFY$*0e@d$%Vce2qAY06KDPHS>bddDBB=dJY(p|!l zV!4}mQ5%+@hU!JorPh(FLmXRIdrhU-PG$X+75FS1nu1xovAtRcEoqJ!Cy=;5KH0AZ zBh-10uP^zP2Fix{ihA^i%8Y+1L6+u+dT{X03+D#*9_su(v$o6S;_H$hc5-`C1cO5< zj6l}j6~0U$vct%~{E?5$7Cmpbt;8}l3BTNq$#6tYw)er1gJKbmm6#hS}cpR((uly7$Tw5WlSxo39H zKskRmFi20U!dhZaZQZOL-rmVD_)s1cL$A@or0}mwH#Xs3Ptn?}9M8T#%d`>}x(JJ5 zL?pG8`tH+Fu0Sr*@`0b91pIx4{FD3Qz+Wccri~}+Ox&wC!MnR|Z2j{qFv;{5vM14{ zmpkh&Cr|P)+O)f0f3|yKP#?O1>&Lf)@o{NR7Wy6kY-)t%?iBBqCG|zm=8Vt_!Q4x% zL+hyl*W^4?T`QBZXQ$ZSl-{2Gul-u7eSMQh9!#QlHF0BmH)0Bad1K8aFIu1%=uPZw zTOd%meHt`_r^p%33b%lf0EMK-wGpQHnqd`jc~$=+V%Fzt`g-K^q^EuMT<>uXd+U1O zdHScNeKg1|#3S z>2CE@%gVi8%gV}|w*|GJ2I+}FxikA?Vtm;xSFymWEUj6!CT1;US=Ia~t?r~bjcdhP z`u>CKx}7kZI%rpHD*OX5{33aP`sdJt)B+8V@$Q#KUC2vq?8D*afn6`moGss$orSr3 zRmDQD^n8tjjn}}+bOB?T0F?Za?Uz97+6BM;bG;v23cD8**3xIm2j-(m{q#@F~)>`W74RBBBt08Z*np!S-f~RC4 z>4v6OD?eVrhV<<_$ksH2C%PQyccn&Gx{Y~%ext$N|_TpLZp0CUuuwX~Gc*nPm zarO*P*wHN@mI-UJIOFy7J@ihr)iZbT;6?d*Q8_bz^UmI5S843+{w6@lu==kltX5aM z5xr!oLKSeFPwDLcO`BwU#n1M&pLiOzI?O?8Z5Z|Ku09o=>T`>@BzG!ah{xRQ_3G=D zPy*Dd^wC)pMI(Z)p!T+yt+3>GD$kl3e`e7AA`v3lokM;b zJ2g6-YPwlaN7uYJ@?~Rfj;8A?|3F6GWl^ee$x^Zh_92{Hn~I?wLIf69ZQqB z#ySe205#7Y-xYD5sSv0_{aPkI+gwxrQnhQO>bAr;*ZYNI(!&e3u|YaRB@iSu9Nin& z>{0?TqZ$c0Q(53{@8MNPQuPr+#5PW!>XUSVQN~I=S`$UTXFR`*0ZWZL(&sso`xZ*TW%!xw67(Kebz%tL}&KaAPYU7Wy{4b=-~$Ccy5zS zniNvxF@pMCjQ&E89mjlYRKqJsQ?VIoLbFb$bWL|2K!lsZ>~4yyJ0*sqbb0@JaPhRy zWX^P*YwT(u^2d*+y#w|AJbg)i5cF}w4!khj-G}B|y4ZQk+QX5U_qq1^YC{>7UY)Bs zQpz{lMa@=a!xYNccTve$&73eGycKnliqaiZemfZc=n<@vI}V?jKi;Ni`113`hlhzM zzGyid0en*Xae(y%XDdsO!$J-o(L#j@(PgMS22HR!v4OZVvNclXPbJJHDf<#IK4OH} zsZS31+eNC|*s+FnOt^vYY;2IHqi>+A8F?+}28`>QEAa4lu|$=J{deZs2R+w~iR3w| z5(ltB1i`iRehT29q;VB)nmw8pWMeqd@tK6+{AU!7ne{9Sj?LA`-jq;$p3|pOlY5;A z;&!t1BAhd^9yc`AKjhCc~-3JsFlWdu0#Bg-efyo-@ znsGN-3Fz0QpbF7=JlzzE(^x()MuLf$wyPnN;IdjnGpId)P2aCG*Ye&zqDm#-VVg> zSmIPT?)h4$0^$qZsI28Xxs;w=Xuwe5anR~MTPzDLejqEg4YbatLNU!XTbkoiLOad zYKMg8Jz*QcJ67+Vi7O1V4?#^4lKh#V)*qnOE$bA8MJNemxih~%y?VPKQfXt)8DMLK z4=+jj$sGKxw(P+o6ge&Z2FQ_LDTW1X?G!p;>ayGD2=U?_Y)iipFT~wFLZ#@;vC>SL zcHDeN*wN*+&g4DdDUKl!HmXIJ9(Zu&5P9^(jF&FF@^alsjxb2OI&xD`aUHsJ^n~Z| zrugy~JRtx^n!+wE;`2}Hq#%xu?jGMQPLZlB+2)>l^W4g+vZswtJ@K#lKEx>Z2>D|J`<47f7m8p6d}+v0`^QJ)zP;+fT$2iR@+^`nX_=<2w>s2$ ztg*Nm4-97A)`@!ZS5WyWLJ{+E>(ar`LO}1wrIz9FMk4%8vK~P8y^E(3@|8x(cExvN zs4ec40@*j%fGxZ zw3Jek8q^gbn{$Z{s8?q)j$mvA_OkL-CmFSdn?}2>W%N_!(jDmpK=02qU9^?b#P>*j zAFR5cJF9>nbXBxbt{Z}Sn{f0c{b#yG1{kNX)s*fJVOo^6|3-TY@H(aX)yiVbH7H}_ z6XfTx>_glSi$LDMDd5_k)lGHAIOO*Qt2~jevK_C7gYKRSP5VYU*!KdyDVV`bvZyhY z$ioP^GGAiN&jXLQ7-g2Sj{**E`!cr*I721ZWRh6>5K8)IpKN1OGHt7VnzBrCaNDsi zGx6Dw65IWi1OaKIso**IcyLy9(-+rN8Rz@Zxv(MLni5Qxizu1qK%5JLAB%A;_+p;c z>Uue3Ln6*w6(7I4*?<11b`Kq@l!L|anO>(``b|BJt+cmJw{^VY!aj1y&_Q=f%surK zpYGvKa8k%%FGI?~lIKYmw{j<@S9Ujj{C|7E_u`6E>W>M5o*^1V)B`xKC2#B&VEZVI173X&C3H zYG{7f2l+AW8@Ksk4Z9{?E6gOZLBfSE0@Zhw4a&BFnG9L9A)-FwzMVW8PbCX5T4mOr z`vyOUd9S1YVAP@1LA5Vb8c(&rD5dExX8o&eX#E!96K;>GZ&duQ+yzsGBa2h`ZyJhe znUS){MH_I@5?s7b|AABI8=09_7(ioK@~ew{U?az`N2oV}tn=_hfAP4-Fcg)KKMFv6=N&=u`%FO_#onAxBPJ2@mtlIUu1}^O`r~sH@tC z+G$}0>xre15M=qL4v`lZeQAH!30AY_)W3Qfpe+pYh`BsQ@a*BIKYLZXZY7=O9MLxR zo+hoZMA&F^3`zcO^tO)jt2xvz4g(kTF*@zaA$9hNhoGbmVSiK1t+WL6M$Y447qfdu z6q9Hg>0Xo}_V2vJixX_unooqkQR|BL>)F;Ox=c5i-B5nIHUf#5{Z}qk!+mdhF6M5w z_+dVOOLxEHTej((_`z2*GR?R8NJ#Z@tt>cy20nB^mFl;Bl7(r9_^1O@b}E!E2A}Uo zx^#BISslJIZ2OrkuKcw0+xRXI3b-Of2Nt=;)z!0WDyJ(eP4Ie^yS3HVz-{@!e7>|uyS~)^iQYd^EPnoI zlydk|CFS_8LFxTc{%5OE^m0|hZ*p-s7)6!s)|gG3I%i-vSkoYV<6~|pUi*-5y_Ql|_ItL?x0w!0npe=(lWHHp6hm63&yXfUao^(Hbm9!o_HXKuf! zP)^DZd{%9~o@@F~1af^^#9IC0|DG{Lr~Wn0bm}#a;58UOAe{Ql-=cKyLVtNQ&-Q8G zx<0l*;lslws6^wd|IOzH%jxrC0FM-y+bFZ0F_ZEKK6=hmA0u^h!AM$~dSZ3H-n;dl zPsyt`^4~hxjVpuqRx2$O z*k40uDDU@CFFqK{|8V(l(oGClhcN}o%pO_!`uIo~mtB^U?X$pq>z$f#ON_#llUK6G zJ>URt7~$EFAoBB)R|p;dCKOT^Je9Eo83ytxFUWT(%dk}|yL*{WjoAdATt3QWOlzVT zqciwoo@x0gb1SR57uvhvP_2o67HtFr>`HWb9^OuAn1?8C+aa8pQ3| z+0OZPC+4p&1TAX@{AzxR`f-C>TTFJvGh)WZDtXQxkqxOaq8Yj6iZ}^n>*;mp^hB8UQ5Vir!p1F5uzgcwf*GuU7J44sxE*4#ZO#ho?uj97n6bmfMSHB;%XT93#8!=5X>9(1|ukt0I3kBd=%!=^M;K1o%Ya&+^-1t|vO9iFMo*4@8Sr9_xAUD_s0dmpzeLF~acw+^zi+Q&57`z-4w*+-~N|Wl(Py@no#&hiExn8DqxxYHQ!iZQ^)o87N~8 z5{Y55iFJKrqjGllDv3u$qo~RM@)rK)m(+-BI>61&$AMlo$mifXCMQv|*SO!B?LPIy zc-xYaO_S~=PO&LPWIiK*?93StH42N2fb9(0mtp#Nm%xS7_`h(h?GE=}-G~y;kxA6`h;!l>(SC=*{R6L zU5h5;)f?xO&(qXfxY0$<OxSLT0EY!1$)2JG3Pk*@T^bw%*;3aY$(_u+%K$0pd(KCLD|X!fOrUdXen3XZj(O z@BZrh@>a42LvjUrjszEp3c{l^q!Q-{0QBT0*TpJE~|LhlE1U_K$3lI%o8IOA;l57Jm zVqQePS?huv6*={|nJ5KNs6x$^K~j{fUpg{a;X0$CG}ouk1kDtN#!|j_47?DX`I$;2 zm*Y8scdjg_V!j~P^K%T8xEM(-Rro>N{_q$9k@6)&LNJo2P0tBa4k@NID|cV?@Tq|e zlQe`q|MpHmJR&fu0WZ`B$Pr=D_Lb!v(R%NEPQ3ltq1i%Hq}}NV8BCGMCaE+F zTd9Y?dl;HXT;TrKa(ICX$S}&S-4X3yl7*<~FX-{goP<&cQg z5`{%W$KKkqnRH$V7x1lv{C)a+xqInC$P(=W5?g_Y^#Nc4D_KsCq34c&3VU2Okun0((%;4Z8=L9t* zw>B-3BU~f2VB}ddb+*KU52fiEZ5O%$IVav9Hy{0nMDA~ens$DWpN<&42Z=3TL$7RV z#M5rL``$Rh``+vlS@y;nSvP<5uEF)!Qz7bJC5loV6Y+QBR#l0~hUyC9ec#_Pi z#?)vLLv5DaMNZ&Vu(kJ}Nr>$31!mq`pJq$E=OLEuw~M8kDr5O%E!8^^ug(MX(yitz z|E|>dP6*qo9_FjyHiLBl`C`_CXjBMeI_hT}7w>JUFvMxcXAyVoi;@(Qq6<^D$sdW~ zaZd0d=viXcJMT&?5DSVk-*~ps`okI$ZPrTS?v4?#?Ab!1+1(y3yI?Yb;5I!qtsUJ^ z98cYNt>X(!y~3miYA_n$Z(MfC{tF7E9MP(Q3o4{Jtf76Bl^0S?<_5{PR^=#Jf~tYc zZBh<@RKll)D~EuF_*t+n=!4+DCCU@X&Y*-fB%Pm zwJ64Dn^x8|ELF~{Al0TO)xEj<{-|)8$dnDQ2)sU5##ck+fo&w9ici77_bGzQP41E%4qLbs_u59*l~m&xF(DdPS0h2DASIrYMvq39;t++Vyd^xQnya=6E; zX=4riq`F{&OD;wyflYjv+D?$Vst>JT_A<@U)0mObosmK06~>srH?FwfOFPmEg|LDr z>;Khi7=_KHx{ma=l~QFV+|mXw>V^Dv)mqSUg+|%5^VYHau!i(r9m`PA)jQo_E_~U_ zJnZYmHtGBDjFi4P;R)wlnM+&fOcnU6z?mxFJZ-uhS`H>#8K%mKi`<(}v2eyT;WGEC zdz$irm$G#^^@--#)cy~elKA%tO=cydxuA;0+A&hPpNfgEbJ`^T`S^#5s$Fm5`~g(o zvA9^oGY@E&F;z$_bN>a=JbB=4O!Y1f?oaz9M_Uz`ZMu+Kqppkx4nKqmy)&#ZIsq%g z?_KTuOo^Dfebl|Dg`~@;s2LJMVIyojjTuy1e%4CNp@U~Ey(cc)qZ*UPe~rGzgbl-; zQ?>_O*9m87T$vCay3Yw^dLn6-<&>FPWSV5$ zg#LjnnAvq;xt+WLgLnz`1-E@?mm;xb;E}|nl*iyh_-6GN*gs?s#e)u@FP2Nz-%NqF z*P#e>D&(2ei~S)T-p(^zZ5a5p@TF2*?#?iHmp!~na5tkMw_qHi*c%{pxqkXjToDg9 z)cYLR&uAMX*j|M5ZB{(s9dUcy)4uYQ>S z?8^No_~XBT|1RA_{)G(z|AXEAzfahGxxb0ux%@UedzC`rv?~GMf$RG>CqFqJ!lx%p zKv2aCqTlc^+@zU#idh+5igQJuc8q4WAK4vXzp*O(Irb7UAurT^R8mD?sI+{u;5wng zZn`x*mK(&cf8gBn*Sz3wwLFJpiSOhfrJV!S=wCPGbQMs!{$Ov6QVT-4c%dvXj~5|T zcEoS$@rmKo6e*@RlkBLk7Ip%sIUfxif5i6_{!BR3W%hUHCoBvvQ&Y)9vQY0~So(iz zsrcbG-Y-_i`*3HlM~v=^p*B8%$9r!;jQ_k5V0{BrpbytGt&yMhXNS&0;s2aya*d&($8(-L8#*t_zu~Ea zuYRO`y`N*r)qFET|7o;N-feo~pB{0r@yn9$QS|Amtu;i5w4cis84)72PC~fbwu^QD ze^~kos5*M4;UdM0yO!eaP~6?!-Q687v`BHcqQ%|eVi$LJcXzwEe?0I1pR+rgNhXuc zoMe*BCbK%1A!36Q+Wmrye;rl1uqz5%XB|SyY;URy>#DB>P>fTQj~&$nD|p9$Z>_+i z%r3gY`i!3n4RdZ?TR{*{1xV5NS#U08K>9eBMM|=7qFrN0@#h@X=rb^kdpF~fTEC2g zYwd&Lkj&`XyN3)&NmdtDNH?j()Exb=`G?MzOh|?RFa{*MxFV(NkYA$52(`YmsrrFk zl2S<4^PJAo9D{9hbDJW7Vg3BYc=JBnGs*Mgjs}BG>i(qiTF2Y%&`vQ*%+xx4!RPgshj7N(0EQ-XzM4=&v_}pkvK>ef_(W_LH&)gZ7o$#YlcY z!TnnMxK;UvuU3bHAWI4nuv!;mpFOeC=W_eGm80&+zVB2X=e6D>lzmEq_?j1a(P3`N z|AJ#PGcyGb7xCs?WwWHm&{&N1cQp^tw;PMZ*o6sd{?QJSLi~QzVjg{SG|5iD1Z5P? zjq;~|P?Uy-z(6G!zHEfbOTR?yT4d-D`JH`m*=mYeXa$CweyH)x9>R6oHo5|>)k*K! zOy#orrW&mk7r?7=OEoY&@Jf9>VCLu-yxZgG$)jSZ%sBb94_MJtnBShb&Y&)C_4b=; zsSp^S12&;}R@mfacKm7Tu&tX^_^G#9LwPb={r70L^X~}Wl-xd&U6Bh62Syewmue@9 zb&rs<`bwV!Ft7I`;#<7zqApEp~EH6sUvYbk%HS1f62q2lvw*nH%CPx290xE(IIDKE9@O#fU9%mLCz;kY zqDr+;^}ur@NmI$d<_sK}K~WF9I~Wq06d7{RuTrEjc~T3`@suo|ocp8dp(R

n1n^m5mNE3@WZ$f&;r3e#Af^>-{6NOBYP8 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

+ /// + /// + public static class IRoutingInputsExtensions + { + /// + /// Gets any existing route for a destination, clears it, and then + /// + public static void ReleaseAndMakeRoute(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType) + { + var sw = new Stopwatch(); + sw.Start(); + + destination.ReleaseRoute(); + + if (source == null) return; + var newRoute = destination.GetRouteToSource(source, signalType); + if (newRoute == null) return; + RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(newRoute); + Debug.Console(2, destination, "Executing new route"); + newRoute.ExecuteRoutes(); + sw.Stop(); + Debug.Console(2, destination, "Route took {0} ms", sw.ElapsedMilliseconds); + } + + /// + /// Will release the existing route on the destination + /// + /// + /// + public static void ReleaseRoute(this IRoutingInputs destination) + { + var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination); + if (current != null) + { + Debug.Console(2, destination, "Releasing current route: {0}", current.Source.Key); + current.ReleaseRoutes(); + } + } + + + /// + /// + /// + /// + /// + /// + /// + public static RouteDescriptor GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType) + { + var routeTable = new RouteDescriptor (source, destination, signalType); + + Debug.Console(0, destination, "Attempting to build source route from {0}***", source.Key); + if (!destination.GetRouteToSource(source, null, null, signalType, 0, routeTable)) + routeTable = null; + + Debug.Console(0, destination, "Route{0} discovered ***", routeTable == null ? " NOT" : ""); + return routeTable; + } + + /// + /// The recursive part of this. Will stop on each device, search its inputs for the + /// desired source and if not found, invoke this function for the each input port + /// hoping to find the source. + /// + /// + /// + /// + /// + /// + /// + /// + /// true if source is hit + static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, + RoutingOutputPort onSuccessOutputPort, List alreadyCheckedDevices, + eRoutingSignalType signalType, int cycle, RouteDescriptor routeTable) + { + cycle++; + Debug.Console(0, destination, "SelectInput-cycle {1}. Finding {2} route back to {0}", source.Key, cycle, signalType); + var destDevInputTies = TieLineCollection.Default.Where(t => + t.DestinationPort.ParentDevice == destination && (t.Type == signalType || t.Type == eRoutingSignalType.AudioVideo)); + + // find a direct tie + var directTie = destDevInputTies.FirstOrDefault( + t => !(t.SourcePort.ParentDevice is IRoutingInputsOutputs) + && t.DestinationPort.ParentDevice == destination + && t.SourcePort.ParentDevice == source); + RoutingInputPort inputPort = null; + if (directTie != null) // Found a tie directly to the source + { + Debug.Console(0, destination, "Found direct tie to {0}**", source.Key); + inputPort = directTie.DestinationPort; + } + else // no direct-connect. Walk back devices. + { + Debug.Console(0, destination, "is not directly connected to {0}. Walking down tie lines", source.Key); + + // No direct tie? Run back out on the inputs' attached devices... + // Only the ones that are routing devices + var attachedMidpoints = destDevInputTies.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs); + foreach (var inputTieToTry in attachedMidpoints) + { + Debug.Console(0, destination, "Trying to find route on {0}", inputTieToTry.SourcePort.ParentDevice.Key); + var upstreamDeviceOutputPort = inputTieToTry.SourcePort; + var upstreamRoutingDevice = upstreamDeviceOutputPort.ParentDevice as IRoutingInputsOutputs; + // Check if this previous device has already been walked + if (!(alreadyCheckedDevices != null && alreadyCheckedDevices.Contains(upstreamRoutingDevice))) + { + // haven't seen this device yet. Do it. Pass the output port to the next + // level to enable switching on success + var upstreamRoutingSuccess = upstreamRoutingDevice.GetRouteToSource(source, upstreamDeviceOutputPort, + alreadyCheckedDevices, signalType, cycle, routeTable); + if (upstreamRoutingSuccess) + { + Debug.Console(0, destination, "Upstream device route found"); + inputPort = inputTieToTry.DestinationPort; + break; // Stop looping the inputs in this cycle + } + } + } + } + + // we have a route on corresponding inputPort. *** Do the route *** + if (inputPort != null) + { + Debug.Console(0, destination, "adding route:"); + if (onSuccessOutputPort == null) + { + // it's a sink device + routeTable.Routes.Add(new RouteSwitchDescriptor(inputPort)); + } + else if (destination is IRouting) + { + routeTable.Routes.Add(new RouteSwitchDescriptor (onSuccessOutputPort, inputPort)); + } + else // device is merely IRoutingInputOutputs + Debug.Console(0, destination, " No routing. Passthrough device"); + Debug.Console(0, destination, "Exiting cycle {0}", cycle); + return true; + } + + if(alreadyCheckedDevices == null) + alreadyCheckedDevices = new List(); + alreadyCheckedDevices.Add(destination as IRoutingInputsOutputs); + + Debug.Console(0, destination, "No route found to {0}", source.Key); + return false; + } + } + + + + + + // MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE + + + /// + /// A collection of routes - typically the global DefaultCollection is used + /// + public class RouteDescriptorCollection + { + public static RouteDescriptorCollection DefaultCollection + { + get + { + if (_DefaultCollection == null) + _DefaultCollection = new RouteDescriptorCollection(); + return _DefaultCollection; + } + } + static RouteDescriptorCollection _DefaultCollection; + + List RouteDescriptors = new List(); + + public void AddRouteDescriptor(RouteDescriptor descriptor) + { + if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination)) + { + Debug.Console(1, descriptor.Destination, + "Route to [{0}] already exists in global routes table", descriptor.Source.Key); + return; + } + RouteDescriptors.Add(descriptor); + } + + public RouteDescriptor GetRouteDescriptorForDestination(IRoutingInputs destination) + { + return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination); + } + + /// + /// Returns the RouteDescriptor for a given destination and removes it from collection. + /// Returns null if no route with the provided destination exists. + /// + public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs destination) + { + var descr = GetRouteDescriptorForDestination(destination); + if (descr != null) + RouteDescriptors.Remove(descr); + return descr; + } + } + + /// + /// Represents an collection of individual route steps between Source and Destination + /// + public class RouteDescriptor + { + public IRoutingInputs Destination { get; private set; } + public IRoutingOutputs Source { get; private set; } + public eRoutingSignalType SignalType { get; private set; } + public List Routes { get; private set; } + + + public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) + { + Destination = destination; + Source = source; + SignalType = signalType; + Routes = new List(); + } + + public void ExecuteRoutes() + { + foreach (var route in Routes) + { + Debug.Console(2, route.ToString()); + if (route.SwitchingDevice is IRoutingSinkWithSwitching) + (route.SwitchingDevice as IRoutingSinkWithSwitching).ExecuteSwitch(route.InputPort.Selector); + else if (route.SwitchingDevice is IRouting) + { + (route.SwitchingDevice as IRouting).ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); + route.OutputPort.InUseTracker.AddUser(Destination, "destination"); + } + } + } + + public void ReleaseRoutes() + { + foreach (var route in Routes) + { + if (route.SwitchingDevice is IRouting) + { + // Pull the route from the port. Whatever is watching the output's in use tracker is + // responsible for responding appropriately. + route.OutputPort.InUseTracker.RemoveUser(Destination, "destination"); + } + } + } + + public override string ToString() + { + var routesText = Routes.Select(r => r.ToString()).ToArray(); + return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); + } + } + + /// + /// Represents an individual link for a route + /// + public class RouteSwitchDescriptor + { + public IRoutingInputs SwitchingDevice { get { return InputPort.ParentDevice; } } + public RoutingOutputPort OutputPort { get; set; } + public RoutingInputPort InputPort { get; set; } + + public RouteSwitchDescriptor(RoutingInputPort inputPort) + { + InputPort = inputPort; + } + + public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort) + { + InputPort = inputPort; + OutputPort = outputPort; + } + + public override string ToString() + { + if(OutputPort == null) // IRoutingSink + return string.Format("{0} switches to input '{1}'", SwitchingDevice.Key, InputPort.Selector); + + return string.Format("{0} switches output '{1}' to input '{2}'", SwitchingDevice.Key, OutputPort.Selector, InputPort.Selector); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingInterfaces.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingInterfaces.cs new file mode 100644 index 00000000..352a35dc --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingInterfaces.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + //******************************************************************************************* + // Interfaces + + public interface IRoutingInputs : IKeyed + { + RoutingPortCollection InputPorts { get; } + } + + public interface IRoutingOutputs : IKeyed + { + RoutingPortCollection OutputPorts { get; } + } + + /// + /// For fixed-source endpoint devices + /// + public interface IRoutingSinkNoSwitching : IRoutingInputs + { + + } + + public interface IRoutingSinkWithSwitching : IRoutingSinkNoSwitching + { + //void ClearRoute(); + void ExecuteSwitch(object inputSelector); + } + + /// + /// For devices like RMCs, baluns, other devices with no switching. + /// + public interface IRoutingInputsOutputs : IRoutingInputs, IRoutingOutputs + { + } + + public interface IRouting : IRoutingInputsOutputs + { + //void ClearRoute(object outputSelector); + void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType); + } + + public interface IRoutingSource : IRoutingOutputs + { + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingPort.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingPort.cs new file mode 100644 index 00000000..e2a026ab --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingPort.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// Base class for RoutingInput and Output ports + /// + public abstract class RoutingPort : IKeyed + { + public string Key { get; private set; } + public eRoutingSignalType Type { get; private set; } + public eRoutingPortConnectionType ConnectionType { get; private set; } + public readonly object Selector; + public bool IsInternal { get; private set; } + + public RoutingPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, object selector, bool isInternal) + { + Key = key; + Type = type; + ConnectionType = connType; + Selector = selector; + IsInternal = IsInternal; + } + } + + public enum eRoutingSignalType + { + Audio, + Video, + AudioVideo + } + + public enum eRoutingPortConnectionType + { + None, BackplaneOnly, Hdmi, Rgb, Vga, LineAudio, DigitalAudio, Sdi, Composite, Component, DmCat, DmMmFiber, DmSmFiber + } + + /// + /// Basic RoutingInput with no statuses. + /// + public class RoutingInputPort : RoutingPort + { + /// + /// The IRoutingInputs object this lives on + /// + public IRoutingInputs ParentDevice { get; private set; } + + /// + /// Constructor for a basic RoutingInputPort + /// + /// An object used to refer to this port in the IRouting device's ExecuteSwitch method. + /// May be string, number, whatever + /// The IRoutingInputs object this lives on + public RoutingInputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, + object selector, IRoutingInputs parent) + : this (key, type, connType, selector, parent, false) + { + } + + /// + /// Constructor for a virtual routing input port that lives inside a device. For example + /// the ports that link a DM card to a DM matrix bus + /// + /// true for internal ports + public RoutingInputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, + object selector, IRoutingInputs parent, bool isInternal) + : base(key, type, connType, selector, isInternal) + { + if (parent == null) + throw new ArgumentNullException("parent"); + ParentDevice = parent; + } + + } + + /// + /// A RoutingInputPort for devices like DM-TX and DM input cards. + /// Will provide video statistics on connected signals + /// + public class RoutingInputPortWithVideoStatuses : RoutingInputPort + { + /// + /// Video statuses attached to this port + /// + public VideoStatusOutputs VideoStatus { get; private set; } + + /// + /// Constructor + /// + /// An object used to refer to this port in the IRouting device's ExecuteSwitch method. + /// May be string, number, whatever + /// The IRoutingInputs object this lives on + /// A VideoStatusFuncsWrapper used to assign the callback funcs that will get + /// the values for the various stats + public RoutingInputPortWithVideoStatuses(string key, + eRoutingSignalType type, eRoutingPortConnectionType connType, object selector, + IRoutingInputs parent, VideoStatusFuncsWrapper funcs) : + base(key, type, connType, selector, parent) + { + VideoStatus = new VideoStatusOutputs(funcs); + } + } + + public class RoutingOutputPort : RoutingPort + { + /// + /// The IRoutingOutputs object this port lives on + /// + public IRoutingOutputs ParentDevice { get; private set; } + + public InUseTracking InUseTracker { get; private set; } + + + /// + /// + /// An object used to refer to this port in the IRouting device's ExecuteSwitch method. + /// May be string, number, whatever + /// The IRoutingOutputs object this port lives on + public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, + object selector, IRoutingOutputs parent) + : this(key, type, connType, selector, parent, false) + { + } + + public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, + object selector, IRoutingOutputs parent, bool isInternal) + : base(key, type, connType, selector, isInternal) + { + if (parent == null) + throw new ArgumentNullException("parent"); + ParentDevice = parent; + InUseTracker = new InUseTracking(); + } + + public override string ToString() + { + return ParentDevice.Key + ":" + Key; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingPortCollection.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingPortCollection.cs new file mode 100644 index 00000000..ba972ab7 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/RoutingPortCollection.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// Basically a List , with an indexer to find ports by key name + /// + public class RoutingPortCollection : List where T: RoutingPort + { + /// + /// Case-insensitive port lookup linked to ports' keys + /// + public T this[string key] + { + get + { + return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/TieLine.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/TieLine.cs new file mode 100644 index 00000000..6a89d2de --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/TieLine.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public class TieLine + { + public RoutingOutputPort SourcePort { get; private set; } + public RoutingInputPort DestinationPort { get; private set; } + public int InUseCount { get { return DestinationUsingThis.Count; } } + + /// + /// Gets the type of this tie line. Will either be the type of the desination port + /// or the type of OverrideType when it is set. + /// + public eRoutingSignalType Type + { + get + { + if (OverrideType.HasValue) return OverrideType.Value; + return DestinationPort.Type; + } + } + + /// + /// Use this to override the Type property for the destination port. For example, + /// when the tie line is type AudioVideo, and the signal flow should be limited to + /// Audio-only or Video only, changing this type will alter the signal paths + /// available to the routing algorithm without affecting the actual Type + /// of the destination port. + /// + public eRoutingSignalType? OverrideType { get; set; } + + List DestinationUsingThis = new List(); + + /// + /// For tie lines that represent internal links, like from cards to the matrix in a DM. + /// This property is true if SourcePort and DestinationPort IsInternal + /// property are both true + /// + public bool IsInternal { get { return SourcePort.IsInternal && DestinationPort.IsInternal; } } + public bool TypeMismatch { get { return SourcePort.Type != DestinationPort.Type; } } + public bool ConnectionTypeMismatch { get { return SourcePort.ConnectionType != DestinationPort.ConnectionType; } } + public string TypeMismatchNote { get; set; } + + /// + /// + /// + /// + /// + public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort) + { + if (sourcePort == null || destinationPort == null) + throw new ArgumentNullException("source or destination port"); + SourcePort = sourcePort; + DestinationPort = destinationPort; + } + + /// + /// Creates a tie line with an overriding Type. See help for OverrideType property for info + /// + /// The signal type to limit the link to. Overrides DestinationPort.Type + public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort, eRoutingSignalType overrideType) : + this(sourcePort, destinationPort) + { + OverrideType = overrideType; + } + + public static TieLine TieLineFromStrings(string sourceKey, string sourcePortKey, string destinationKey, string destinationPortKey) + { + var sourceDev = DeviceManager.GetDeviceForKey(sourceKey) as IRoutingOutputs; + if (sourceDev == null) + { + Debug.Console(1, "WARNING: Cannot create tie line, routable source '{0}' not found", sourceKey); + return null; + } + var destDev = DeviceManager.GetDeviceForKey(destinationKey) as IRoutingInputs; + if (destDev == null) + { + Debug.Console(1, "WARNING: Cannot create tie line, routable destination '{0}' not found", destinationKey); + return null; + } + var sourcePort = sourceDev.OutputPorts[sourcePortKey]; + if (sourcePort == null) + { + Debug.Console(1, "WARNING: Cannot create tie line. Source '{0}' does not contain port '{1}'", sourceKey, sourcePortKey); + return null; + } + var destPort = destDev.InputPorts[destinationPortKey]; + if (destPort == null) + { + Debug.Console(1, "WARNING: Cannot create tie line. Destination '{0}' does not contain port '{1}'", destinationKey, destinationPortKey); + return null; + } + + return new TieLine(sourcePort, destPort); + } + + /// + /// Will link up video status from supporting inputs to connected outputs + /// + public void Activate() + { + // Now does nothing + } + + public void Deactivate() + { + // Now does nothing + } + + public override string ToString() + { + return string.Format("Tie line: [{0}]{1} --> [{2}]{3}", SourcePort.ParentDevice.Key, SourcePort.Key, + DestinationPort.ParentDevice.Key, DestinationPort.Key); + } + } + + + //******************************************************************************** + + public class TieLineCollection : List + { + public static TieLineCollection Default + { + get + { + if (_Default == null) + _Default = new TieLineCollection(); + return _Default; + } + } + static TieLineCollection _Default; + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/DummyRoutingInputsDevice.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/DummyRoutingInputsDevice.cs new file mode 100644 index 00000000..9a92c2ea --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/DummyRoutingInputsDevice.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.Routing +{ + public class DummyRoutingInputsDevice : Device, IRoutingSource + { + /// + /// A single output port, backplane, audioVideo + /// + public RoutingOutputPort AudioVideoOutputPort { get; private set; } + + /// + /// contains the output port + /// + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection() { AudioVideoOutputPort }; } + } + + /// + /// constructor + /// + /// key for special device + public DummyRoutingInputsDevice(string key) : base(key) + { + AudioVideoOutputPort = new RoutingOutputPort("internal", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.BackplaneOnly, + null, this, true); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/ICardPortsDevice.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/ICardPortsDevice.cs new file mode 100644 index 00000000..6f9ea22f --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/ICardPortsDevice.cs @@ -0,0 +1,20 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; + +//using PepperDash.Core; + +//namespace PepperDash.Essentials.Core +//{ +// /// +// /// Defines a class that has cards, like a DM chassis controller, where +// /// we need to access ports on those cards +// /// +// public interface ICardPortsDevice : IKeyed +// { +// RoutingInputPort GetChildInputPort(string card, string port); +// RoutingOutputPort GetChildOutputPort(string card, string port); +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/IRoutingInputsExtensions.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/IRoutingInputsExtensions.cs new file mode 100644 index 00000000..d92a0202 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/IRoutingInputsExtensions.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// Extensions added to any IRoutingInputs classes to provide discovery-based routing + /// on those destinations. + /// + public static class IRoutingInputsExtensions + { + /// + /// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute + /// and then attempts a new Route and if sucessful, stores that RouteDescriptor + /// in RouteDescriptorCollection.DefaultCollection + /// + public static void ReleaseAndMakeRoute(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType) + { + destination.ReleaseRoute(); + + if (source == null) return; + var newRoute = destination.GetRouteToSource(source, signalType); + if (newRoute == null) return; + RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(newRoute); + Debug.Console(2, destination, "Executing full route"); + newRoute.ExecuteRoutes(); + } + + /// + /// Will release the existing route on the destination, if it is found in + /// RouteDescriptorCollection.DefaultCollection + /// + /// + public static void ReleaseRoute(this IRoutingInputs destination) + { + var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination); + if (current != null) + { + Debug.Console(1, destination, "Releasing current route: {0}", current.Source.Key); + current.ReleaseRoutes(); + } + } + + /// + /// Builds a RouteDescriptor that contains the steps necessary to make a route between devices. + /// Routes of type AudioVideo will be built as two separate routes, audio and video. If + /// a route is discovered, a new RouteDescriptor is returned. If one or both parts + /// of an audio/video route are discovered a route descriptor is returned. If no route is + /// discovered, then null is returned + /// + public static RouteDescriptor GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType) + { + var routeDescr = new RouteDescriptor(source, destination, signalType); + // if it's a single signal type, find the route + if (signalType != eRoutingSignalType.AudioVideo) + { + Debug.Console(1, destination, "Attempting to build source route from {0}", source.Key); + if (!destination.GetRouteToSource(source, null, null, signalType, 0, routeDescr)) + routeDescr = null; + } + // otherwise, audioVideo needs to be handled as two steps. + else + { + Debug.Console(1, destination, "Attempting to build audio and video routes from {0}", source.Key); + var audioSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Audio, 0, routeDescr); + if (!audioSuccess) + Debug.Console(1, destination, "Cannot find audio route to {0}", source.Key); + var videoSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Video, 0, routeDescr); + if (!videoSuccess) + Debug.Console(1, destination, "Cannot find video route to {0}", source.Key); + if (!audioSuccess && !videoSuccess) + routeDescr = null; + } + + //Debug.Console(1, destination, "Route{0} discovered", routeDescr == null ? " NOT" : ""); + return routeDescr; + } + + /// + /// The recursive part of this. Will stop on each device, search its inputs for the + /// desired source and if not found, invoke this function for the each input port + /// hoping to find the source. + /// + /// + /// + /// The RoutingOutputPort whose link is being checked for a route + /// Prevents Devices from being twice-checked + /// This recursive function should not be called with AudioVideo + /// Just an informational counter + /// The RouteDescriptor being populated as the route is discovered + /// true if source is hit + static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, + RoutingOutputPort outputPortToUse, List alreadyCheckedDevices, + eRoutingSignalType signalType, int cycle, RouteDescriptor routeTable) + { + cycle++; + Debug.Console(2, "GetRouteToSource: {0} {1}--> {2}", cycle, source.Key, destination.Key); + + RoutingInputPort goodInputPort = null; + var destDevInputTies = TieLineCollection.Default.Where(t => + t.DestinationPort.ParentDevice == destination && (t.Type == signalType || t.Type == eRoutingSignalType.AudioVideo)); + + // find a direct tie + var directTie = destDevInputTies.FirstOrDefault( + t => t.DestinationPort.ParentDevice == destination + && t.SourcePort.ParentDevice == source); + if (directTie != null) // Found a tie directly to the source + { + goodInputPort = directTie.DestinationPort; + } + else // no direct-connect. Walk back devices. + { + Debug.Console(2, destination, "is not directly connected to {0}. Walking down tie lines", source.Key); + + // No direct tie? Run back out on the inputs' attached devices... + // Only the ones that are routing devices + var attachedMidpoints = destDevInputTies.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs); + foreach (var inputTieToTry in attachedMidpoints) + { + Debug.Console(2, destination, "Trying to find route on {0}", inputTieToTry.SourcePort.ParentDevice.Key); + var upstreamDeviceOutputPort = inputTieToTry.SourcePort; + var upstreamRoutingDevice = upstreamDeviceOutputPort.ParentDevice as IRoutingInputsOutputs; + // Check if this previous device has already been walked + if (!(alreadyCheckedDevices != null && alreadyCheckedDevices.Contains(upstreamRoutingDevice))) + { + // haven't seen this device yet. Do it. Pass the output port to the next + // level to enable switching on success + var upstreamRoutingSuccess = upstreamRoutingDevice.GetRouteToSource(source, upstreamDeviceOutputPort, + alreadyCheckedDevices, signalType, cycle, routeTable); + if (upstreamRoutingSuccess) + { + Debug.Console(2, destination, "Upstream device route found"); + goodInputPort = inputTieToTry.DestinationPort; + break; // Stop looping the inputs in this cycle + } + } + } + } + + // we have a route on corresponding inputPort. *** Do the route *** + if (goodInputPort != null) + { + //Debug.Console(2, destination, "adding RouteDescriptor"); + if (outputPortToUse == null) + { + // it's a sink device + routeTable.Routes.Add(new RouteSwitchDescriptor(goodInputPort)); + } + else if (destination is IRouting) + { + routeTable.Routes.Add(new RouteSwitchDescriptor (outputPortToUse, goodInputPort)); + } + else // device is merely IRoutingInputOutputs + Debug.Console(2, destination, " No routing. Passthrough device"); + //Debug.Console(2, destination, "Exiting cycle {0}", cycle); + return true; + } + + if(alreadyCheckedDevices == null) + alreadyCheckedDevices = new List(); + alreadyCheckedDevices.Add(destination as IRoutingInputsOutputs); + + Debug.Console(2, destination, "No route found to {0}", source.Key); + return false; + } + } + + + + + + // MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE MOVE + + + /// + /// A collection of RouteDescriptors - typically the static DefaultCollection is used + /// + public class RouteDescriptorCollection + { + public static RouteDescriptorCollection DefaultCollection + { + get + { + if (_DefaultCollection == null) + _DefaultCollection = new RouteDescriptorCollection(); + return _DefaultCollection; + } + } + static RouteDescriptorCollection _DefaultCollection; + + List RouteDescriptors = new List(); + + /// + /// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the + /// destination exists already, it will not be added - in order to preserve + /// proper route releasing. + /// + /// + public void AddRouteDescriptor(RouteDescriptor descriptor) + { + if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination)) + { + Debug.Console(1, descriptor.Destination, + "Route to [{0}] already exists in global routes table", descriptor.Source.Key); + return; + } + RouteDescriptors.Add(descriptor); + } + + /// + /// Gets the RouteDescriptor for a destination + /// + /// null if no RouteDescriptor for a destination exists + public RouteDescriptor GetRouteDescriptorForDestination(IRoutingInputs destination) + { + return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination); + } + + /// + /// Returns the RouteDescriptor for a given destination AND removes it from collection. + /// Returns null if no route with the provided destination exists. + /// + public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs destination) + { + var descr = GetRouteDescriptorForDestination(destination); + if (descr != null) + RouteDescriptors.Remove(descr); + return descr; + } + } + + /// + /// Represents an collection of individual route steps between Source and Destination + /// + public class RouteDescriptor + { + public IRoutingInputs Destination { get; private set; } + public IRoutingOutputs Source { get; private set; } + public eRoutingSignalType SignalType { get; private set; } + public List Routes { get; private set; } + + + public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) + { + Destination = destination; + Source = source; + SignalType = signalType; + Routes = new List(); + } + + /// + /// Executes all routes described in this collection. Typically called via + /// extension method IRoutingInputs.ReleaseAndMakeRoute() + /// + public void ExecuteRoutes() + { + foreach (var route in Routes) + { + Debug.Console(2, "ExecuteRoutes: {0}", route.ToString()); + if (route.SwitchingDevice is IRoutingSinkWithSwitching) + (route.SwitchingDevice as IRoutingSinkWithSwitching).ExecuteSwitch(route.InputPort.Selector); + else if (route.SwitchingDevice is IRouting) + { + (route.SwitchingDevice as IRouting).ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); + route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType); + Debug.Console(2, "Output port {0} routing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); + } + } + } + + /// + /// Releases all routes in this collection. Typically called via + /// extension method IRoutingInputs.ReleaseAndMakeRoute() + /// + public void ReleaseRoutes() + { + foreach (var route in Routes) + { + if (route.SwitchingDevice is IRouting) + { + // Pull the route from the port. Whatever is watching the output's in use tracker is + // responsible for responding appropriately. + route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType); + Debug.Console(2, "Port {0} releasing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); + } + } + } + + public override string ToString() + { + var routesText = Routes.Select(r => r.ToString()).ToArray(); + return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); + } + } + + /// + /// Represents an individual link for a route + /// + public class RouteSwitchDescriptor + { + public IRoutingInputs SwitchingDevice { get { return InputPort.ParentDevice; } } + public RoutingOutputPort OutputPort { get; set; } + public RoutingInputPort InputPort { get; set; } + + public RouteSwitchDescriptor(RoutingInputPort inputPort) + { + InputPort = inputPort; + } + + public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort) + { + InputPort = inputPort; + OutputPort = outputPort; + } + + public override string ToString() + { + if(SwitchingDevice is IRouting) + return string.Format("{0} switches output '{1}' to input '{2}'", SwitchingDevice.Key, OutputPort.Selector, InputPort.Selector); + else + return string.Format("{0} switches to input '{1}'", SwitchingDevice.Key, InputPort.Selector); + + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingInterfaces.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingInterfaces.cs new file mode 100644 index 00000000..76745f22 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingInterfaces.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + //******************************************************************************************* + // Interfaces + + + /// + /// Defines a class that has a collection of RoutingInputPorts + /// + public interface IRoutingInputs : IKeyed + { + RoutingPortCollection InputPorts { get; } + } + + /// + /// Defines a class that has a collection of RoutingOutputPorts + /// + + public interface IRoutingOutputs : IKeyed + { + RoutingPortCollection OutputPorts { get; } + } + + /// + /// For fixed-source endpoint devices + /// + public interface IRoutingSinkNoSwitching : IRoutingInputs + { + + } + + /// + /// Endpoint device like a display, that selects inputs + /// + public interface IRoutingSinkWithSwitching : IRoutingSinkNoSwitching + { + //void ClearRoute(); + void ExecuteSwitch(object inputSelector); + } + + /// + /// For devices like RMCs, baluns, other devices with no switching. + /// + public interface IRoutingInputsOutputs : IRoutingInputs, IRoutingOutputs + { + } + + /// + /// Defines a midpoint device as have internal routing. Any devices in the middle of the + /// signal chain, that do switching, must implement this for routing to work otherwise + /// the routing algorithm will treat the IRoutingInputsOutputs device as a passthrough + /// device. + /// + public interface IRouting : IRoutingInputsOutputs + { + //void ClearRoute(object outputSelector); + void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType); + } + + public interface ITxRouting : IRouting + { + IntFeedback VideoSourceNumericFeedback { get; } + IntFeedback AudioSourceNumericFeedback { get; } + void ExecuteNumericSwitch(ushort input, ushort output, eRoutingSignalType type); + } + + /// + /// Defines an IRoutingOutputs devices as being a source - the start of the chain + /// + public interface IRoutingSource : IRoutingOutputs + { + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPort.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPort.cs new file mode 100644 index 00000000..47e47501 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPort.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// Base class for RoutingInput and Output ports + /// + public abstract class RoutingPort : IKeyed + { + public string Key { get; private set; } + public eRoutingSignalType Type { get; private set; } + public eRoutingPortConnectionType ConnectionType { get; private set; } + public readonly object Selector; + public bool IsInternal { get; private set; } + public object FeedbackMatchObject { get; set; } + public object Port { get; set; } + + public RoutingPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, object selector, bool isInternal) + { + Key = key; + Type = type; + ConnectionType = connType; + Selector = selector; + IsInternal = IsInternal; + } + } + + public enum eRoutingSignalType + { + Audio = 1, + Video = 2, + AudioVideo = 3 + } + + public enum eRoutingPortConnectionType + { + None, BackplaneOnly, DisplayPort, Dvi, Hdmi, Rgb, Vga, LineAudio, DigitalAudio, Sdi, + Composite, Component, DmCat, DmMmFiber, DmSmFiber, Speaker, Streaming + } + + /// + /// Basic RoutingInput with no statuses. + /// + public class RoutingInputPort : RoutingPort + { + /// + /// The IRoutingInputs object this lives on + /// + public IRoutingInputs ParentDevice { get; private set; } + + /// + /// Constructor for a basic RoutingInputPort + /// + /// An object used to refer to this port in the IRouting device's ExecuteSwitch method. + /// May be string, number, whatever + /// The IRoutingInputs object this lives on + public RoutingInputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, + object selector, IRoutingInputs parent) + : this (key, type, connType, selector, parent, false) + { + } + + /// + /// Constructor for a virtual routing input port that lives inside a device. For example + /// the ports that link a DM card to a DM matrix bus + /// + /// true for internal ports + public RoutingInputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, + object selector, IRoutingInputs parent, bool isInternal) + : base(key, type, connType, selector, isInternal) + { + if (parent == null) + throw new ArgumentNullException("parent"); + ParentDevice = parent; + } + + + + + ///// + ///// Static method to get a named port from a named device + ///// + ///// Returns null if device or port doesn't exist + //public static RoutingInputPort GetDevicePort(string deviceKey, string portKey) + //{ + // var sourceDev = DeviceManager.GetDeviceForKey(deviceKey) as IRoutingInputs; + // if (sourceDev == null) + // return null; + // return sourceDev.InputPorts[portKey]; + //} + + ///// + ///// Static method to get a named port from a card in a named ICardPortsDevice device + ///// Uses ICardPortsDevice.GetChildInputPort + ///// + ///// 'input-N' + ///// null if device, card or port doesn't exist + //public static RoutingInputPort GetDeviceCardPort(string deviceKey, string cardKey, string portKey) + //{ + // var sourceDev = DeviceManager.GetDeviceForKey(deviceKey) as ICardPortsDevice; + // if (sourceDev == null) + // return null; + // return sourceDev.GetChildInputPort(cardKey, portKey); + //} + } + + /// + /// A RoutingInputPort for devices like DM-TX and DM input cards. + /// Will provide video statistics on connected signals + /// + public class RoutingInputPortWithVideoStatuses : RoutingInputPort + { + /// + /// Video statuses attached to this port + /// + public VideoStatusOutputs VideoStatus { get; private set; } + + /// + /// Constructor + /// + /// An object used to refer to this port in the IRouting device's ExecuteSwitch method. + /// May be string, number, whatever + /// The IRoutingInputs object this lives on + /// A VideoStatusFuncsWrapper used to assign the callback funcs that will get + /// the values for the various stats + public RoutingInputPortWithVideoStatuses(string key, + eRoutingSignalType type, eRoutingPortConnectionType connType, object selector, + IRoutingInputs parent, VideoStatusFuncsWrapper funcs) : + base(key, type, connType, selector, parent) + { + VideoStatus = new VideoStatusOutputs(funcs); + } + } + + public class RoutingOutputPort : RoutingPort + { + /// + /// The IRoutingOutputs object this port lives on + /// + public IRoutingOutputs ParentDevice { get; private set; } + + public InUseTracking InUseTracker { get; private set; } + + + /// + /// + /// An object used to refer to this port in the IRouting device's ExecuteSwitch method. + /// May be string, number, whatever + /// The IRoutingOutputs object this port lives on + public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, + object selector, IRoutingOutputs parent) + : this(key, type, connType, selector, parent, false) + { + } + + public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, + object selector, IRoutingOutputs parent, bool isInternal) + : base(key, type, connType, selector, isInternal) + { + if (parent == null) + throw new ArgumentNullException("parent"); + ParentDevice = parent; + InUseTracker = new InUseTracking(); + } + + public override string ToString() + { + return ParentDevice.Key + ":" + Key; + } + + ///// + ///// Static method to get a named port from a named device + ///// + ///// Returns null if device or port doesn't exist + //public static RoutingOutputPort GetDevicePort(string deviceKey, string portKey) + //{ + // var sourceDev = DeviceManager.GetDeviceForKey(deviceKey) as IRoutingOutputs; + // if (sourceDev == null) + // return null; + // var port = sourceDev.OutputPorts[portKey]; + // if (port == null) + // Debug.Console(0, "WARNING: Device '{0}' does does not contain output port '{1}'", deviceKey, portKey); + // return port; + //} + + ///// + ///// Static method to get a named port from a card in a named ICardPortsDevice device + ///// Uses ICardPortsDevice.GetChildOutputPort on that device + ///// + ///// 'input-N' or 'output-N' + ///// null if device, card or port doesn't exist + //public static RoutingOutputPort GetDeviceCardPort(string deviceKey, string cardKey, string portKey) + //{ + // var sourceDev = DeviceManager.GetDeviceForKey(deviceKey) as ICardPortsDevice; + // if (sourceDev == null) + // return null; + // var port = sourceDev.GetChildOutputPort(cardKey, portKey); + //} + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortCollection.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortCollection.cs new file mode 100644 index 00000000..ba972ab7 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortCollection.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// Basically a List , with an indexer to find ports by key name + /// + public class RoutingPortCollection : List where T: RoutingPort + { + /// + /// Case-insensitive port lookup linked to ports' keys + /// + public T this[string key] + { + get + { + return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortNames.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortNames.cs new file mode 100644 index 00000000..00e85191 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortNames.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core.Routing +{ + /// + /// These should correspond directly with the portNames var in the config tool. + /// + public class RoutingPortNames + { + /// + /// antennaIn + /// + public const string AntennaIn = "antennaIn"; + /// + /// anyAudioIn + /// + public const string AnyAudioIn = "anyAudioIn"; + /// + /// anyAudioOut + /// + public const string AnyAudioOut = "anyAudioOut"; + /// + /// anyOut + /// + public const string AnyOut = "anyOut"; + /// + /// anyVideoIn + /// + public const string AnyVideoIn = "anyVideoIn"; + /// + /// anyVideoOut + /// + public const string AnyVideoOut = "anyVideoOut"; + /// + /// balancedAudioOut + /// + public const string BalancedAudioOut = "balancedAudioOut"; + /// + /// codecOsd + /// + public const string CodecOsd = "codecOsd"; + /// + /// componentIn + /// + public const string ComponentIn = "componentIn"; + /// + /// componentOut + /// + public const string ComponentOut = "componentOut"; + /// + /// compositeIn + /// + public const string CompositeIn = "compositeIn"; + /// + /// compositeOut + /// + public const string CompositeOut = "compositeOut"; + /// + /// displayPortIn + /// + public const string DisplayPortIn = "displayPortIn"; + /// + /// displayPortIn1 + /// + public const string DisplayPortIn1 = "displayPortIn1"; + /// + /// displayPortIn2 + /// + public const string DisplayPortIn2 = "displayPortIn2"; + /// + /// displayPortIn3 + /// + public const string DisplayPortIn3 = "displayPortIn3"; + /// + /// displayPortOut + /// + public const string DisplayPortOut = "displayPortOut"; + /// + /// dmIn + /// + public const string DmIn = "dmIn"; + /// + /// dmOut + /// + public const string DmOut = "dmOut"; + /// + /// dviIn + /// + public const string DviIn = "dviIn"; + /// + /// dviIn1 + /// + public const string DviIn1 = "dviIn1"; + /// + /// dviOut + /// + public const string DviOut = "dviOut"; + /// + /// hdmiIn + /// + public const string HdmiIn = "hdmiIn"; + /// + /// hdmiIn1 + /// + public const string HdmiIn1 = "hdmiIn1"; + /// + /// hdmiIn1PC + /// + public const string HdmiIn1PC = "hdmiIn1PC"; + /// + /// hdmiIn2 + /// + public const string HdmiIn2 = "hdmiIn2"; + /// + /// hdmiIn2PC + /// + public const string HdmiIn2PC = "hdmiIn2PC"; + /// + /// hdmiIn3 + /// + public const string HdmiIn3 = "hdmiIn3"; + /// + /// hdmiIn4 + /// + public const string HdmiIn4 = "hdmiIn4"; + /// + /// hdmiIn5 + /// + public const string HdmiIn5 = "hdmiIn5"; + /// + /// hdmiIn6 + /// + public const string HdmiIn6 = "hdmiIn6"; + /// + /// hdmiOut + /// + public const string HdmiOut = "hdmiOut"; + /// + /// hdmiOut1 + /// + public const string HdmiOut1 = "hdmiOut1"; + /// + /// hdmiOut2 + /// + public const string HdmiOut2 = "hdmiOut2"; + /// + /// hdmiOut3 + /// + public const string HdmiOut3 = "hdmiOut3"; + /// + /// hdmiOut4 + /// + public const string HdmiOut4 = "hdmiOut4"; + /// + /// hdmiOut5 + /// + public const string HdmiOut5 = "hdmiOut5"; + /// + /// hdmiOut6 + /// + public const string HdmiOut6 = "hdmiOut6"; + /// + /// none + /// + public const string None = "none"; + /// + /// rgbIn + /// + public const string RgbIn = "rgbIn"; + /// + /// rgbIn1 + /// + public const string RgbIn1 = "rgbIn1"; + /// + /// rgbIn2 + /// + public const string RgbIn2 = "rgbIn2"; + /// + /// vgaIn + /// + public const string VgaIn = "vgaIn"; + /// + /// vgaIn1 + /// + public const string VgaIn1 = "vgaIn1"; + /// + /// vgaOut + /// + public const string VgaOut = "vgaOut"; + /// + /// IPC/OPS + /// + public const string IpcOps = "ipcOps"; + /// + /// MediaPlayer + /// + public const string MediaPlayer = "mediaPlayer"; + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/TieLine.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/TieLine.cs new file mode 100644 index 00000000..25bf4ea3 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/TieLine.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public class TieLine + { + public RoutingOutputPort SourcePort { get; private set; } + public RoutingInputPort DestinationPort { get; private set; } + //public int InUseCount { get { return DestinationUsingThis.Count; } } + + /// + /// Gets the type of this tie line. Will either be the type of the desination port + /// or the type of OverrideType when it is set. + /// + public eRoutingSignalType Type + { + get + { + if (OverrideType.HasValue) return OverrideType.Value; + return DestinationPort.Type; + } + } + + /// + /// Use this to override the Type property for the destination port. For example, + /// when the tie line is type AudioVideo, and the signal flow should be limited to + /// Audio-only or Video only, changing this type will alter the signal paths + /// available to the routing algorithm without affecting the actual Type + /// of the destination port. + /// + public eRoutingSignalType? OverrideType { get; set; } + + //List DestinationUsingThis = new List(); + + /// + /// For tie lines that represent internal links, like from cards to the matrix in a DM. + /// This property is true if SourcePort and DestinationPort IsInternal + /// property are both true + /// + public bool IsInternal { get { return SourcePort.IsInternal && DestinationPort.IsInternal; } } + public bool TypeMismatch { get { return SourcePort.Type != DestinationPort.Type; } } + public bool ConnectionTypeMismatch { get { return SourcePort.ConnectionType != DestinationPort.ConnectionType; } } + public string TypeMismatchNote { get; set; } + + /// + /// + /// + /// + /// + public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort) + { + if (sourcePort == null || destinationPort == null) + throw new ArgumentNullException("source or destination port"); + SourcePort = sourcePort; + DestinationPort = destinationPort; + } + + /// + /// Creates a tie line with an overriding Type. See help for OverrideType property for info + /// + /// The signal type to limit the link to. Overrides DestinationPort.Type + public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort, eRoutingSignalType overrideType) : + this(sourcePort, destinationPort) + { + OverrideType = overrideType; + } + + /// + /// Will link up video status from supporting inputs to connected outputs + /// + public void Activate() + { + // Now does nothing + } + + public void Deactivate() + { + // Now does nothing + } + + public override string ToString() + { + return string.Format("Tie line: [{0}]{1} --> [{2}]{3}", SourcePort.ParentDevice.Key, SourcePort.Key, + DestinationPort.ParentDevice.Key, DestinationPort.Key); + } + } + + //******************************************************************************** + + public class TieLineCollection : List + { + public static TieLineCollection Default + { + get + { + if (_Default == null) + _Default = new TieLineCollection(); + return _Default; + } + } + static TieLineCollection _Default; + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/TieLineConfig.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/TieLineConfig.cs new file mode 100644 index 00000000..d0087747 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing/TieLineConfig.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharpPro; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Core.Config +{ + public class TieLineConfig + { + public string SourceKey { get; set; } + public string SourceCard { get; set; } + public string SourcePort { get; set; } + public string DestinationKey { get; set; } + public string DestinationCard { get; set; } + public string DestinationPort { get; set; } + + /// + /// Returns the appropriate tie line for either a card-based device or + /// regular device with ports on-device. + /// + /// null if config data does not match ports, cards or devices + public TieLine GetTieLine() + { + Debug.Console(0, "Build TieLine: {0}", this); + // Get the source device + var sourceDev = DeviceManager.GetDeviceForKey(SourceKey) as IRoutingOutputs; + if (sourceDev == null) + { + LogError("Routable source not found"); + return null; + } + + // Get the destination device + var destDev = DeviceManager.GetDeviceForKey(DestinationKey) as IRoutingInputs; + if (destDev == null) + { + LogError("Routable destination not found"); + return null; + } + + //Get the source port + RoutingOutputPort sourceOutputPort = null; + //// If it's a card-based device, get the card and then the source port + //if (sourceDev is ICardPortsDevice) + //{ + // if (SourceCard == null) + // { + // LogError("Card missing from source device config"); + // return null; + // } + // sourceOutputPort = (sourceDev as ICardPortsDevice).GetChildOutputPort(SourceCard, SourcePort); + // if (sourceOutputPort == null) + // { + // LogError("Source card does not contain port"); + // return null; + // } + //} + //// otherwise it's a normal port device, get the source port + //else + //{ + sourceOutputPort = sourceDev.OutputPorts[SourcePort]; + if (sourceOutputPort == null) + { + LogError("Source does not contain port"); + return null; + } + //} + + + //Get the Destination port + RoutingInputPort destinationInputPort = null; + //// If it's a card-based device, get the card and then the Destination port + //if (destDev is ICardPortsDevice) + //{ + // if (DestinationCard == null) + // { + // LogError("Card missing from destination device config"); + // return null; + // } + // destinationInputPort = (destDev as ICardPortsDevice).GetChildInputPort(DestinationCard, DestinationPort); + // if (destinationInputPort == null) + // { + // LogError("Destination card does not contain port"); + // return null; + // } + //} + //// otherwise it's a normal port device, get the Destination port + //else + //{ + destinationInputPort = destDev.InputPorts[DestinationPort]; + if (destinationInputPort == null) + { + LogError("Destination does not contain port"); + return null; + } + //} + + return new TieLine(sourceOutputPort, destinationInputPort); + } + + void LogError(string msg) + { + Debug.Console(1, "WARNING: Cannot create tie line: {0}:\r {1}", msg, this); + } + + public override string ToString() + { + return string.Format("{0}.{1}.{2} --> {3}.{4}.{5}", SourceKey, SourceCard, SourcePort, + DestinationKey, DestinationCard, DestinationPort); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/Shade Interfaces.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/Shade Interfaces.cs new file mode 100644 index 00000000..b4d7c88d --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/Shade Interfaces.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.Shades +{ + /// + /// Requirements for an object that contains shades + /// + public interface IShades + { + List Shades { get; } + } + + /// + /// Requirements for a device that implements basic Open/Close shade control + /// + public interface IShadesOpenClose + { + void Open(); + void Close(); + } + + /// + /// Requirements for a device that implements basic Open/Close/Stop shade control (Uses 3 relays) + /// + public interface IShadesOpenCloseStop : IShadesOpenClose + { + void StopOrPreset(); + } + + /// + /// Requirements for a shade that implements press/hold raise/lower functions + /// + public interface IShadesRaiseLower + { + void Raise(bool state); + void Lower(bool state); + } + + /// + /// Requirements for a shade device that provides raising/lowering feedback + /// + public interface IShadesRaiseLowerFeedback + { + BoolFeedback ShadeIsLoweringFeedback { get; } + BoolFeedback ShadeIsRaisingFeedback { get; } + } + + /// + /// Requirements for a shade/scene that is open or closed + /// + public interface IShadesOpenClosedFeedback: IShadesOpenClose + { + BoolFeedback ShadeIsOpenFeedback { get; } + BoolFeedback ShadeIsClosedFeedback { get; } + } + + /// + /// + /// + public interface IShadesStop + { + void Stop(); + } + + /// + /// + /// + public interface IShadesStopOrMove + { + void OpenOrStop(); + void CloseOrStop(); + void OpenCloseOrStop(); + } + + /// + /// Basic feedback for shades/scene stopped + /// + public interface IShadesStopFeedback + { + BoolFeedback IsStoppedFeedback { get; } + } + + /// + /// Requirements for position + /// + public interface IShadesPosition + { + void SetPosition(ushort value); + } + + /// + /// Basic feedback for shades position + /// + public interface IShadesFeedback: IShadesPosition, IShadesStopFeedback + { + IntFeedback PositionFeedback { get; } + } + + /// + /// + /// + public interface ISceneFeedback + { + void Run(); + BoolFeedback AllAreAtSceneFeedback { get; } + } + + public interface ICrestronBasicShade : IShadesOpenClosedFeedback, IShadesStop, + IShadesStopOrMove, IShadesFeedback, IShadesRaiseLowerFeedback + { + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/ShadeBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/ShadeBase.cs new file mode 100644 index 00000000..636a5440 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/ShadeBase.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core.CrestronIO; + +namespace PepperDash.Essentials.Core.Shades +{ + /// + /// Base class for a shade device + /// + public abstract class ShadeBase : Device, IShadesOpenClose + { + public ShadeBase(string key, string name) + : base(key, name) + { + + } + + #region iShadesOpenClose Members + + public abstract void Open(); + public abstract void StopOrPreset(); + public abstract void Close(); + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/ShadeController.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/ShadeController.cs new file mode 100644 index 00000000..b226af05 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Shades/ShadeController.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.Shades +{ + /// + /// Class that contains the shades to be controlled in a room + /// + public class ShadeController : Device, IShades + { + ShadeControllerConfigProperties Config; + + public List Shades { get; private set; } + + public ShadeController(string key, string name, ShadeControllerConfigProperties config) + : base(key, name) + { + Config = config; + + Shades = new List(); + } + + public override bool CustomActivate() + { + foreach (var shadeConfig in Config.Shades) + { + var shade = DeviceManager.GetDeviceForKey(shadeConfig.Key) as ShadeBase; + + if (shade != null) + { + AddShade(shade); + } + } + return base.CustomActivate(); + } + + void AddShade(ShadeBase shade) + { + Shades.Add(shade); + } + } + + public class ShadeControllerConfigProperties + { + public List Shades { get; set; } + + + public class ShadeConfig + { + public string Key { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/SigHelper.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SigHelper.cs new file mode 100644 index 00000000..86395114 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SigHelper.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// Helper class for various Sig events + /// + public class SigHelper + { + /// + /// Runs action when Sig is pressed + /// + /// + public static void Pressed(Sig sig, Action act) { if (sig.BoolValue) act(); } + + /// + /// Runs action when Sig is released + /// + public static void Released(Sig sig, Action act) { if (!sig.BoolValue) act(); } + + /// + /// Safely sets an action to non-null sig + /// + public static void SetBoolOutAction(BoolOutputSig sig, Action a) + { + if (sig != null) + sig.UserObject = a; + } + + /// + /// Safely clears action of non-null sig. + /// + public static void ClearBoolOutAction(BoolOutputSig sig) + { + if (sig != null) + sig.UserObject = null; + } + + /// + /// Does a timed ramp, where the time is scaled proportional to the + /// remaining range to cover + /// + /// Ushort sig to scale + /// Level to go to + /// In ms (not hundredths like Crestron Sig ramp function) + public static void RampTimeScaled(Sig sig, ushort newLevel, uint time) + { + ushort level = sig.UShortValue; + int diff = Math.Abs(level - newLevel); + uint scaledTime = (uint)(diff * time / 65535); + Ramp(sig, newLevel, scaledTime); + } + + /// + /// Ramps signal + /// + /// + /// + /// In ms (not hundredths like Crestron Sig ramp function) + public static void Ramp(Sig sig, ushort level, uint time) + { + sig.CreateRamp(level, time / 10); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectDPad.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectDPad.cs new file mode 100644 index 00000000..2307be7e --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectDPad.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +namespace PepperDash.Essentials.Core.SmartObjects +{ + public class SmartObjectDPad : SmartObjectHelperBase + { + public BoolOutputSig SigUp { get { return GetBoolOutputNamed("Up"); } } + public BoolOutputSig SigDown { get { return GetBoolOutputNamed("Down"); } } + public BoolOutputSig SigLeft { get { return GetBoolOutputNamed("Left"); } } + public BoolOutputSig SigRight { get { return GetBoolOutputNamed("Right"); } } + public BoolOutputSig SigCenter { get { return GetBoolOutputNamed("Center"); } } + + public SmartObjectDPad(SmartObject so, bool useUserObjectHandler) + : base(so, useUserObjectHandler) + { + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectDynamicList.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectDynamicList.cs new file mode 100644 index 00000000..bf73ba1b --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectDynamicList.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core.SmartObjects +{ + public class SmartObjectDynamicList : SmartObjectHelperBase + { + public const string SigNameScrollToItem = "Scroll To Item"; + public const string SigNameSetNumberOfItems = "Set Number of Items"; + + public uint NameSigOffset { get; private set; } + + public ushort Count + { + get + { + return SmartObject.UShortInput[SigNameSetNumberOfItems].UShortValue; + } + set { SmartObject.UShortInput[SigNameSetNumberOfItems].UShortValue = value; } + } + + /// + /// The limit of the list object, as defined by VTPro settings. Zero if object + /// is not a list + /// + public int MaxCount { get; private set; } + + /// + /// Wrapper for smart object + /// + /// + /// True if the standard user object action handler will be used + /// The starting join of the string sigs for the button labels + public SmartObjectDynamicList(SmartObject so, bool useUserObjectHandler, uint nameSigOffset) : base(so, useUserObjectHandler) + { + try + { + // Just try to touch the count signal to make sure this is indeed a dynamic list + var c = Count; + NameSigOffset = nameSigOffset; + MaxCount = SmartObject.BooleanOutput.Count(s => s.Name.EndsWith("Pressed")); + //Debug.Console(2, "Smart object {0} has {1} max", so.ID, MaxCount); + } + catch + { + var msg = string.Format("SmartObjectDynamicList: Smart Object {0:X2}-{1} is not a dynamic list. Ignoring", so.Device.ID, so.ID); + Debug.Console(0, Debug.ErrorLogLevel.Error, msg); + } + } + + /// + /// Builds a new list item + /// + public void SetItem(uint index, string mainText, string iconName, Action action) + { + SetItemMainText(index, mainText); + SetItemIcon(index, iconName); + SetItemButtonAction(index, action); + //try + //{ + // SetMainButtonText(index, text); + // SetIcon(index, iconName); + // SetButtonAction(index, action); + //} + //catch(Exception e) + //{ + // Debug.Console(1, "Cannot set Dynamic List item {0} on smart object {1}", index, SmartObject.ID); + // ErrorLog.Warn(e.ToString()); + //} + } + + public void SetItemMainText(uint index, string text) + { + if (index > MaxCount) return; + // The list item template defines CIPS tags that refer to standard joins + (SmartObject.Device as BasicTriList).StringInput[NameSigOffset + index].StringValue = text; + } + + public void SetItemIcon(uint index, string iconName) + { + if (index > MaxCount) return; + SmartObject.StringInput[string.Format("Set Item {0} Icon Serial", index)].StringValue = iconName; + } + + public void SetItemButtonAction(uint index, Action action) + { + if (index > MaxCount) return; + SmartObject.BooleanOutput[string.Format("Item {0} Pressed", index)].UserObject = action; + } + + /// + /// Sets the feedback on the given line, clearing others when interlocked is set + /// + public void SetFeedback(uint index, bool interlocked) + { + if (interlocked) + ClearFeedbacks(); + SmartObject.BooleanInput[string.Format("Item {0} Selected", index)].BoolValue = true; + } + + /// + /// Clears all button feedbacks + /// + public void ClearFeedbacks() + { + for(int i = 1; i<= Count; i++) + SmartObject.BooleanInput[string.Format("Item {0} Selected", i)].BoolValue = false; + } + + /// + /// Removes Action object from all buttons + /// + public void ClearActions() + { + Debug.Console(2, "SO CLEAR"); + for(ushort i = 1; i <= MaxCount; i++) + SmartObject.BooleanOutput[string.Format("Item {0} Pressed", i)].UserObject = null; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectHelper.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectHelper.cs new file mode 100644 index 00000000..f30d8da1 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectHelper.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + public class SmartObjectHelper + { + public static uint GetSmartObjectJoinForTypeAndObject(uint sourceType, uint typeOffset) + { + return (uint)(10000 + (sourceType * 100) + typeOffset); + } + + //public static void DumpSmartObject(SmartGraphicsTouchpanelControllerBase tp, uint id) + //{ + // if (!tp.TriList.SmartObjects.Contains(id)) + // { + // Debug.Console(0, tp, "does not contain smart object ID {0}", id); + // return; + // } + // var so = tp.TriList.SmartObjects[id]; + // Debug.Console(0, tp, "Signals for smart object ID {0}", id); + // Debug.Console(0, "BooleanInput -------------------------------"); + // foreach (var s in so.BooleanInput) + // Debug.Console(0, " {0,5} {1}", s.Number, s.Name); + // Debug.Console(0, "UShortInput -------------------------------"); + // foreach (var s in so.UShortInput) + // Debug.Console(0, " {0,5} {1}", s.Number, s.Name); + // Debug.Console(0, "StringInput -------------------------------"); + // foreach (var s in so.StringInput) + // Debug.Console(0, " {0,5} {1}", s.Number, s.Name); + // Debug.Console(0, "BooleanOutput -------------------------------"); + // foreach (var s in so.BooleanOutput) + // Debug.Console(0, " {0,5} {1}", s.Number, s.Name); + // Debug.Console(0, "UShortOutput -------------------------------"); + // foreach (var s in so.UShortOutput) + // Debug.Console(0, " {0,5} {1}", s.Number, s.Name); + // Debug.Console(0, "StringOutput -------------------------------"); + // foreach (var s in so.StringOutput) + // Debug.Console(0, " {0,5} {1}", s.Number, s.Name); + //} + + ///// + ///// Inserts/removes the appropriate UO's onto sigs + ///// + ///// + ///// + ///// + ///// + //public static void LinkNumpadWithUserObjects(BasicTriListWithSmartObject triList, + // uint smartObjectId, List deviceUserObjects, Cue Misc_1Function, Cue Misc_2Function, bool state) + //{ + // var sigDict = new Dictionary + // { + // { "0", CommonBoolCue.Digit0 }, + // { "1", CommonBoolCue.Digit1 }, + // { "2", CommonBoolCue.Digit2 }, + // { "3", CommonBoolCue.Digit3 }, + // { "4", CommonBoolCue.Digit4 }, + // { "5", CommonBoolCue.Digit5 }, + // { "6", CommonBoolCue.Digit6 }, + // { "7", CommonBoolCue.Digit7 }, + // { "8", CommonBoolCue.Digit8 }, + // { "9", CommonBoolCue.Digit9 }, + // { "Misc_1", Misc_1Function }, + // { "Misc_2", Misc_2Function }, + // }; + // LinkSmartObjectWithUserObjects(triList, smartObjectId, deviceUserObjects, sigDict, state); + //} + + //public static void LinkDpadWithUserObjects(BasicTriListWithSmartObject triList, + // uint smartObjectId, List deviceUserObjects, bool state) + //{ + // var sigDict = new Dictionary + // { + // { "Up", CommonBoolCue.Up }, + // { "Down", CommonBoolCue.Down }, + // { "Left", CommonBoolCue.Left }, + // { "Right", CommonBoolCue.Right }, + // { "OK", CommonBoolCue.Select }, + // }; + // LinkSmartObjectWithUserObjects(triList, smartObjectId, deviceUserObjects, sigDict, state); + //} + + + ///// + ///// MOVE TO HELPER CLASS + ///// + ///// + ///// + ///// + ///// + ///// + //public static void LinkSmartObjectWithUserObjects(BasicTriListWithSmartObject triList, + // uint smartObjectId, List deviceUserObjects, Dictionary smartObjectSigMap, bool state) + //{ + // // Hook up smart objects if applicable + // if (triList.SmartObjects.Contains(smartObjectId)) + // { + // var smartObject = triList.SmartObjects[smartObjectId]; + // foreach (var kvp in smartObjectSigMap) + // { + // if (smartObject.BooleanOutput.Contains(kvp.Key)) + // { + // if (state) + // { + // // look for a user object and if so, attach/detach it to/from the sig. + // var uo = deviceUserObjects.FirstOrDefault(ao => ao.Cue == kvp.Value); + // if (uo != null) + // smartObject.BooleanOutput[kvp.Key].UserObject = uo; + // } + // else + // smartObject.BooleanOutput[kvp.Key].UserObject = null; + // } + // else + // Debug.Console(0, "Smart object {0} does not contain Sig {1}", smartObject.ID, kvp.Key); + // } + // } + // else + // Debug.Console(0, "ERROR Smart object {0} not found on {1:X2}", smartObjectId, triList.ID); + //} + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectHelperBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectHelperBase.cs new file mode 100644 index 00000000..6fadf491 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectHelperBase.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.SmartObjects +{ + public class SmartObjectHelperBase + { + public SmartObject SmartObject { get; private set; } + + /// + /// This should be set by all inheriting classes, after the class has verified that it is linked to the right object. + /// + public bool Validated { get; protected set; } + + public SmartObjectHelperBase(SmartObject so, bool useUserObjectHandler) + { + SmartObject = so; + if (useUserObjectHandler) + { + // Prevent this from double-registering + SmartObject.SigChange -= this.SmartObject_SigChange; + SmartObject.SigChange += this.SmartObject_SigChange; + } + } + + ~SmartObjectHelperBase() + { + SmartObject.SigChange -= this.SmartObject_SigChange; + } + + /// + /// Helper to get a sig name with debugging when fail + /// + /// + /// + public BoolOutputSig GetBoolOutputNamed(string name) + { + if (SmartObject.BooleanOutput.Contains(name)) + return SmartObject.BooleanOutput[name]; + else + Debug.Console(0, "WARNING: Cannot get signal. Smart object {0} on trilist {1:x2} does not contain signal '{2}'", + SmartObject.ID, SmartObject.Device.ID, name); + return null; + } + + /// + /// Sets action on signal after checking for existence. + /// + /// + /// + public void SetBoolAction(string name, Action a) + { + if (SmartObject.BooleanOutput.Contains(name)) + SmartObject.BooleanOutput[name].UserObject = a; + else + { + Debug.Console(0, "WARNING: Cannot set action. Smart object {0} on trilist {1:x2} does not contain signal '{2}'", + SmartObject.ID, SmartObject.Device.ID, name); + } + } + + /// + /// Standard Action listener + /// + /// + /// + void SmartObject_SigChange(GenericBase currentDevice, SmartObjectEventArgs args) + { + var uo = args.Sig.UserObject; + if (uo is Action) + (uo as Action)(args.Sig.BoolValue); + else if (uo is Action) + (uo as Action)(args.Sig.UShortValue); + else if (uo is Action) + (uo as Action)(args.Sig.StringValue); + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectNumeric.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectNumeric.cs new file mode 100644 index 00000000..7e574242 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SmartObjectNumeric.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +namespace PepperDash.Essentials.Core.SmartObjects +{ + public class SmartObjectNumeric : SmartObjectHelperBase + { + /// + /// Defaults to "Misc_1". The name of the button in VTPro (Usually the text) + /// + public string Misc1SigName { get; set; } + /// + /// Defaults to "Misc_2". The name of the button in VTPro (Usually the text) + /// + public string Misc2SigName { get; set; } + + public BoolOutputSig Digit1 { get { return GetBoolOutputNamed("1"); } } + public BoolOutputSig Digit2 { get { return GetBoolOutputNamed("2"); } } + public BoolOutputSig Digit3 { get { return GetBoolOutputNamed("3"); } } + public BoolOutputSig Digit4 { get { return GetBoolOutputNamed("4"); } } + public BoolOutputSig Digit5 { get { return GetBoolOutputNamed("5"); } } + public BoolOutputSig Digit6 { get { return GetBoolOutputNamed("6"); } } + public BoolOutputSig Digit7 { get { return GetBoolOutputNamed("7"); } } + public BoolOutputSig Digit8 { get { return GetBoolOutputNamed("8"); } } + public BoolOutputSig Digit9 { get { return GetBoolOutputNamed("9"); } } + public BoolOutputSig Digit0 { get { return GetBoolOutputNamed("0"); } } + public BoolOutputSig Misc1 { get { return GetBoolOutputNamed(Misc1SigName); } } + public BoolOutputSig Misc2 { get { return GetBoolOutputNamed(Misc2SigName); } } + + public SmartObjectNumeric(SmartObject so, bool useUserObjectHandler) : base(so, useUserObjectHandler) + { + Misc1SigName = "Misc_1"; + Misc2SigName = "Misc_2"; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SourceListSubpageReferenceList.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SourceListSubpageReferenceList.cs new file mode 100644 index 00000000..e8c49b40 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SourceListSubpageReferenceList.cs @@ -0,0 +1,167 @@ + + + +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +//using Crestron.SimplSharpPro.UI; + +//using PepperDash.Core; + + +//namespace PepperDash.Essentials.Core +//{ + +// //***************************************************************************** +// /// +// /// Wrapper class for subpage reference list. Contains helpful methods to get at the various signal groupings +// /// and to get individual signals using an index and a join. +// /// +// public class SourceListSubpageReferenceList : SubpageReferenceList +// { +// public const uint SmartObjectJoin = 3801; + +// Action SourceSelectCallback; + +// EssentialsRoom CurrentRoom; + +// public SourceListSubpageReferenceList(BasicTriListWithSmartObject tl, +// Action sourceSelectCallback) +// : base(tl, SmartObjectJoin, 3, 1, 3) +// { +// SourceSelectCallback = sourceSelectCallback; +// } + +// void SetSourceList(Dictionary dict) +// { +// // Iterate all positions, including ones missing from the dict. +// var max = dict.Keys.Max(); +// for (uint i = 1; i <= max; i++) +// { +// // Add the source if it's in the dict +// if (dict.ContainsKey(i)) +// { +// Items.Add(new SourceListSubpageReferenceListItem(i, dict[i], this, SourceSelectCallback)); +// // Plug the callback function into the buttons +// } +// // Blank the line +// else +// Items.Add(new SourceListSubpageReferenceListItem(i, null, +// this, SourceSelectCallback)); +// } +// Count = (ushort)max; +// } + +// /// +// /// Links the SRL to the Room's PresentationSourceChange event for updating of the UI +// /// +// /// +// public void AttachToRoom(EssentialsRoom room) +// { +// CurrentRoom = room; +// SetSourceList(room.Sources); +// CurrentRoom.PresentationSourceChange -= CurrentRoom_PresentationSourceChange; +// CurrentRoom.PresentationSourceChange += CurrentRoom_PresentationSourceChange; +// SetPresentationSourceFb(CurrentRoom.CurrentPresentationSource); +// } + +// /// +// /// Disconnects the SRL from a Room's PresentationSourceChange +// /// +// public void DetachFromCurrentRoom() +// { +// ClearPresentationSourceFb(CurrentRoom.CurrentPresentationSource); +// if(CurrentRoom != null) +// CurrentRoom.PresentationSourceChange -= CurrentRoom_PresentationSourceChange; +// CurrentRoom = null; +// } + +// // Handler to route source changes into list feedback +// void CurrentRoom_PresentationSourceChange(object sender, EssentialsRoomSourceChangeEventArgs args) +// { +// Debug.Console(2, "SRL received source change"); +// ClearPresentationSourceFb(args.OldSource); +// SetPresentationSourceFb(args.NewSource); +// } + +// void ClearPresentationSourceFb(IPresentationSource source) +// { +// if (source == null) return; +// var oldSourceItem = (SourceListSubpageReferenceListItem)Items.FirstOrDefault( +// i => ((SourceListSubpageReferenceListItem)i).SourceDevice == source); +// if (oldSourceItem != null) +// oldSourceItem.ClearFeedback(); +// } + +// void SetPresentationSourceFb(IPresentationSource source) +// { +// if (source == null) return; +// // Now set the new source to light up +// var newSourceItem = (SourceListSubpageReferenceListItem)Items.FirstOrDefault( +// i => ((SourceListSubpageReferenceListItem)i).SourceDevice == source); +// if (newSourceItem != null) +// newSourceItem.SetFeedback(); +// } +// } + +// public class SourceListSubpageReferenceListItem : SubpageReferenceListItem +// { +// public readonly IPresentationSource SourceDevice; + +// public const uint ButtonPressJoin = 1; +// public const uint SelectedFeedbackJoin = 2; +// public const uint ButtonTextJoin = 1; +// public const uint IconNameJoin = 2; + +// public SourceListSubpageReferenceListItem(uint index, +// IPresentationSource srcDevice, SubpageReferenceList owner, Action sourceSelectCallback) +// : base(index, owner) +// { +// if (srcDevice == null) throw new ArgumentNullException("srcDevice"); +// if (owner == null) throw new ArgumentNullException("owner"); +// if (sourceSelectCallback == null) throw new ArgumentNullException("sourceSelectCallback"); + + +// SourceDevice = srcDevice; +// var nameSig = owner.StringInputSig(index, ButtonTextJoin); +// // Should be able to see if there is not enough buttons right here +// if (nameSig == null) +// { +// Debug.Console(0, "ERROR: Item {0} does not exist on source list SRL", index); +// return; +// } +// nameSig.StringValue = srcDevice.Name; +// owner.StringInputSig(index, IconNameJoin).StringValue = srcDevice.IconName; + +// // Assign a source selection action to the appropriate button's UserObject - on release +// owner.GetBoolFeedbackSig(index, ButtonPressJoin).UserObject = new Action(b => +// { if (!b) sourceSelectCallback(index); }); + +// // hook up the video icon +// var videoDev = srcDevice as IAttachVideoStatus; +// if (videoDev != null) +// { +// var status = videoDev.GetVideoStatuses(); +// if (status != null) +// { +// Debug.Console(1, "Linking {0} video status to SRL", videoDev.Key); +// videoDev.GetVideoStatuses().VideoSyncFeedback.LinkInputSig(owner.BoolInputSig(index, 3)); +// } +// } +// } + +// public void SetFeedback() +// { +// Owner.BoolInputSig(Index, SelectedFeedbackJoin).BoolValue = true; +// } + +// public void ClearFeedback() +// { +// Owner.BoolInputSig(Index, SelectedFeedbackJoin).BoolValue = false; +// } +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SubpageReferenceList.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SubpageReferenceList.cs new file mode 100644 index 00000000..2f8794f0 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SubpageReferenceList.cs @@ -0,0 +1,263 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.UI; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + ////***************************************************************************** + ///// + ///// Base class for all subpage reference list controllers + ///// + //public class SubpageReferenceListController + //{ + // public SubpageReferenceList TheList { get; protected set; } + //} + + //***************************************************************************** + /// + /// Wrapper class for subpage reference list. Contains helpful methods to get at the various signal groupings + /// and to get individual signals using an index and a join. + /// + public class SubpageReferenceList + { + + public ushort Count + { + get { return SetNumberOfItemsSig.UShortValue; } + set { SetNumberOfItemsSig.UShortValue = value; } + } + public ushort MaxDefinedItems { get; private set; } + + public UShortInputSig ScrollToItemSig { get; private set; } + UShortInputSig SetNumberOfItemsSig; + public uint BoolIncrement { get; protected set; } + public uint UShortIncrement { get; protected set; } + public uint StringIncrement { get; protected set; } + + protected readonly SmartObject SRL; + protected readonly List Items = new List(); + + public SubpageReferenceList(BasicTriListWithSmartObject triList, uint smartObjectId, + uint boolIncrement, uint ushortIncrement, uint stringIncrement) + { + SmartObject obj; + // Fail cleanly if not defined + if (triList.SmartObjects == null || triList.SmartObjects.Count == 0) + { + Debug.Console(0, "TriList {0:X2} Smart objects have not been loaded", triList.ID, smartObjectId); + return; + } + if (triList.SmartObjects.TryGetValue(smartObjectId, out obj)) + { + SRL = triList.SmartObjects[smartObjectId]; + ScrollToItemSig = SRL.UShortInput["Scroll To Item"]; + SetNumberOfItemsSig = SRL.UShortInput["Set Number of Items"]; + BoolIncrement = boolIncrement; + UShortIncrement = ushortIncrement; + StringIncrement = stringIncrement; + + // Count the enable lines to see what max items is + MaxDefinedItems = (ushort)SRL.BooleanInput + .Where(s => s.Name.Contains("Enable")).Count(); + Debug.Console(2, "SRL {0} contains max {1} items", SRL.ID, MaxDefinedItems); + + SRL.SigChange -= new SmartObjectSigChangeEventHandler(SRL_SigChange); + SRL.SigChange += new SmartObjectSigChangeEventHandler(SRL_SigChange); + } + else + Debug.Console(0, "ERROR: TriList 0x{0:X2} Cannot load smart object {1}. Verify correct SGD file is loaded", + triList.ID, smartObjectId); + } + + /// + /// Adds item to saved list of displayed items (not necessarily in order) + /// DOES NOT adjust Count + /// + /// + public void AddItem(SubpageReferenceListItem item) + { + Items.Add(item); + } + + /// + /// Items need to be responsible for managing their own deallocation process, + /// disconnecting from events, etc. + /// + /// + public void Clear() + { + // If a line item needs to disconnect an CueActionPair or do something to release RAM + foreach (var item in Items) item.Clear(); + // Empty the list + Items.Clear(); + // Clean up the SRL + Count = 0; + ScrollToItemSig.UShortValue = 1; + } + + /// + /// Optional call to refresh the signals on the objects in the SRL - this calls Refresh() on + /// all SubpageReferenceListItem items + /// + public void Refresh() + { + foreach (var item in Items) item.Refresh(); + } + + + // Helpers to get sigs by their weird SO names + + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line or item position on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public BoolOutputSig GetBoolFeedbackSig(uint index, uint sigNum) + { + if (sigNum > BoolIncrement) return null; + return SRL.BooleanOutput.FirstOrDefault(s => s.Name.Equals(GetBoolFeedbackSigName(index, sigNum))); + } + + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line or item position on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public UShortOutputSig GetUShortOutputSig(uint index, uint sigNum) + { + if (sigNum > UShortIncrement) return null; + return SRL.UShortOutput.FirstOrDefault(s => s.Name.Equals(GetBoolFeedbackSigName(index, sigNum))); + } + + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line or item position on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public StringOutputSig GetStringOutputSig(uint index, uint sigNum) + { + if (sigNum > StringIncrement) return null; + return SRL.StringOutput.FirstOrDefault(s => s.Name.Equals(GetBoolFeedbackSigName(index, sigNum))); + } + + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public BoolInputSig BoolInputSig(uint index, uint sigNum) + { + if (sigNum > BoolIncrement) return null; + return SRL.BooleanInput.FirstOrDefault(s => s.Name.Equals(GetBoolInputSigName(index, sigNum))); + } + + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public UShortInputSig UShortInputSig(uint index, uint sigNum) + { + if (sigNum > UShortIncrement) return null; + return SRL.UShortInput.FirstOrDefault(s => s.Name.Equals(GetUShortInputSigName(index, sigNum))); + } + + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public StringInputSig StringInputSig(uint index, uint sigNum) + { + if (sigNum > StringIncrement) return null; + return SRL.StringInput.FirstOrDefault(s => s.Name.Equals(GetStringInputSigName(index, sigNum))); + } + + // Helpers to get signal names + + string GetBoolFeedbackSigName(uint index, uint sigNum) + { + var num = (index - 1) * BoolIncrement + sigNum; + return String.Format("press{0}", num); + } + + string GetUShortOutputSigName(uint index, uint sigNum) + { + var num = (index - 1) * UShortIncrement + sigNum; + return String.Format("an_act{0}", num); + } + + string GetStringOutputSigName(uint index, uint sigNum) + { + var num = (index - 1) * StringIncrement + sigNum; + return String.Format("text-i{0}", num); + } + + string GetBoolInputSigName(uint index, uint sigNum) + { + var num = (index - 1) * BoolIncrement + sigNum; + return String.Format("fb{0}", num); + } + + string GetUShortInputSigName(uint index, uint sigNum) + { + var num = (index - 1) * UShortIncrement + sigNum; + return String.Format("an_fb{0}", num); + } + + string GetStringInputSigName(uint index, uint sigNum) + { + var num = (index - 1) * StringIncrement + sigNum; + return String.Format("text-o{0}", num); + } + + /// + /// Stock SigChange handler + /// + /// + /// + public static void SRL_SigChange(GenericBase currentDevice, SmartObjectEventArgs args) + { + var uo = args.Sig.UserObject; + if (uo is Action) + (uo as Action)(args.Sig.BoolValue); + else if (uo is Action) + (uo as Action)(args.Sig.UShortValue); + else if (uo is Action) + (uo as Action)(args.Sig.StringValue); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SubpageReferenceListItem.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SubpageReferenceListItem.cs new file mode 100644 index 00000000..30e15f74 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/SmartObjects/SubpageReferencList/SubpageReferenceListItem.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.UI; + +namespace PepperDash.Essentials.Core +{ + public class SubpageReferenceListItem + { + /// + /// The list that this lives in + /// + protected SubpageReferenceList Owner; + protected uint Index; + + public SubpageReferenceListItem(uint index, SubpageReferenceList owner) + { + Index = index; + Owner = owner; + } + + /// + /// Called by SRL to release all referenced objects + /// + public virtual void Clear() + { + } + + public virtual void Refresh() { } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Timers/CountdownTimer.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Timers/CountdownTimer.cs new file mode 100644 index 00000000..a8da97e4 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Timers/CountdownTimer.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public class SecondsCountdownTimer: IKeyed + { + public event EventHandler HasStarted; + public event EventHandler HasFinished; + public event EventHandler WasCancelled; + + public string Key { get; private set; } + + public BoolFeedback IsRunningFeedback { get; private set; } + bool _IsRunning; + + public IntFeedback PercentFeedback { get; private set; } + public StringFeedback TimeRemainingFeedback { get; private set; } + + public bool CountsDown { get; set; } + public int SecondsToCount { get; set; } + + public DateTime StartTime { get; private set; } + public DateTime FinishTime { get; private set; } + + CTimer SecondTimer; + + /// + /// + /// + /// + public SecondsCountdownTimer(string key) + { + Key = key; + IsRunningFeedback = new BoolFeedback(() => _IsRunning); + + TimeRemainingFeedback = new StringFeedback(() => + { + // Need to handle up and down here. + + if (StartTime == null || FinishTime == null) + return ""; + var timeSpan = FinishTime - DateTime.Now; + return Math.Round(timeSpan.TotalSeconds).ToString(); + }); + + PercentFeedback = new IntFeedback(() => + { + if (StartTime == null || FinishTime == null) + return 0; + double percent = (FinishTime - DateTime.Now).TotalSeconds + / (FinishTime - StartTime).TotalSeconds + * 100; + return (int)percent; + }); + } + + /// + /// + /// + public void Start() + { + if (_IsRunning) + return; + StartTime = DateTime.Now; + FinishTime = StartTime + TimeSpan.FromSeconds(SecondsToCount); + + if (SecondTimer != null) + SecondTimer.Stop(); + SecondTimer = new CTimer(SecondElapsedTimerCallback, null, 0, 1000); + _IsRunning = true; + IsRunningFeedback.FireUpdate(); + + var handler = HasStarted; + if (handler != null) + handler(this, new EventArgs()); + } + + /// + /// + /// + public void Reset() + { + _IsRunning = false; + Start(); + } + + /// + /// + /// + public void Cancel() + { + StopHelper(); + + var handler = WasCancelled; + if (handler != null) + handler(this, new EventArgs()); + } + + /// + /// Called upon expiration, or calling this will force timer to finish. + /// + public void Finish() + { + StopHelper(); + + var handler = HasFinished; + if (handler != null) + handler(this, new EventArgs()); + } + + void StopHelper() + { + if (SecondTimer != null) + SecondTimer.Stop(); + _IsRunning = false; + IsRunningFeedback.FireUpdate(); + } + + void SecondElapsedTimerCallback(object o) + { + PercentFeedback.FireUpdate(); + TimeRemainingFeedback.FireUpdate(); + + if (DateTime.Now >= FinishTime) + Finish(); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/CrestronTouchpanelPropertiesConfig.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/CrestronTouchpanelPropertiesConfig.cs new file mode 100644 index 00000000..42882a51 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/CrestronTouchpanelPropertiesConfig.cs @@ -0,0 +1,48 @@ +namespace PepperDash.Essentials.Core +{ + public class CrestronTouchpanelPropertiesConfig + { + public string IpId { get; set; } + public string DefaultRoomKey { get; set; } + public string RoomListKey { get; set; } + public string SgdFile { get; set; } + public string ProjectName { get; set; } + public bool ShowVolumeGauge { get; set; } + public bool UsesSplashPage { get; set; } + public bool ShowDate { get; set; } + public bool ShowTime { get; set; } + public UiSetupPropertiesConfig Setup { get; set; } + public string HeaderStyle { get; set; } + public bool IncludeInFusionRoomHealth { get; set; } + + + /// + /// The count of sources that will trigger the "additional" arrows to show on the SRL. + /// Defaults to 5 + /// + public int SourcesOverflowCount { get; set; } + + public CrestronTouchpanelPropertiesConfig() + { + SourcesOverflowCount = 5; + HeaderStyle = CrestronTouchpanelPropertiesConfig.Habanero; + } + + /// + /// "habanero" + /// + public const string Habanero = "habanero"; + /// + /// "verbose" + /// + public const string Verbose = "verbose"; + } + + /// + /// + /// + public class UiSetupPropertiesConfig + { + public bool IsVisible { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/Keyboards/HabaneroKeyboardController.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/Keyboards/HabaneroKeyboardController.cs new file mode 100644 index 00000000..5e068c12 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/Keyboards/HabaneroKeyboardController.cs @@ -0,0 +1,436 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DeviceSupport; + +namespace PepperDash.Essentials.Core.Touchpanels.Keyboards +{ + public class HabaneroKeyboardController + { + /// + /// Single-key press events, rather than using a built-up text string on the OutputFeedback + /// + public event EventHandler KeyPress; + + public BasicTriList TriList { get; private set; } + + public StringFeedback OutputFeedback { get; private set; } + + public bool IsVisible { get; private set; } + + public string DotComButtonString { get; set; } + + public string GoButtonText { get; set; } + + public string SecondaryButtonText { get; set; } + + public bool GoButtonVisible { get; set; } + + public bool SecondaryButtonVisible { get; set; } + + int ShiftMode = 0; + + StringBuilder Output; + + public Action HideAction { get; set; } + + CTimer BackspaceTimer; + + /// + /// + /// + /// + public HabaneroKeyboardController(BasicTriList trilist) + { + TriList = trilist; + Output = new StringBuilder(); + OutputFeedback = new StringFeedback(() => Output.ToString()); + DotComButtonString = ".com"; + } + + /// + /// Shows the keyboard and attaches sig handlers in the range of 2901-2969 + /// + public void Show() + { + if (IsVisible) + return; + + TriList.SetSigTrueAction(ClosePressJoin, Hide); + TriList.SetSigTrueAction(GoButtonPressJoin, () => OnKeyPress(KeyboardSpecialKey.GoButton)); + TriList.SetSigTrueAction(SecondaryButtonPressJoin, () => OnKeyPress(KeyboardSpecialKey.SecondaryButton)); + TriList.SetSigTrueAction(2921, () => Press(A(ShiftMode))); + TriList.SetSigTrueAction(2922, () => Press(B(ShiftMode))); + TriList.SetSigTrueAction(2923, () => Press(C(ShiftMode))); + TriList.SetSigTrueAction(2924, () => Press(D(ShiftMode))); + TriList.SetSigTrueAction(2925, () => Press(E(ShiftMode))); + TriList.SetSigTrueAction(2926, () => Press(F(ShiftMode))); + TriList.SetSigTrueAction(2927, () => Press(G(ShiftMode))); + TriList.SetSigTrueAction(2928, () => Press(H(ShiftMode))); + TriList.SetSigTrueAction(2929, () => Press(I(ShiftMode))); + TriList.SetSigTrueAction(2930, () => Press(J(ShiftMode))); + TriList.SetSigTrueAction(2931, () => Press(K(ShiftMode))); + TriList.SetSigTrueAction(2932, () => Press(L(ShiftMode))); + TriList.SetSigTrueAction(2933, () => Press(M(ShiftMode))); + TriList.SetSigTrueAction(2934, () => Press(N(ShiftMode))); + TriList.SetSigTrueAction(2935, () => Press(O(ShiftMode))); + TriList.SetSigTrueAction(2936, () => Press(P(ShiftMode))); + TriList.SetSigTrueAction(2937, () => Press(Q(ShiftMode))); + TriList.SetSigTrueAction(2938, () => Press(R(ShiftMode))); + TriList.SetSigTrueAction(2939, () => Press(S(ShiftMode))); + TriList.SetSigTrueAction(2940, () => Press(T(ShiftMode))); + TriList.SetSigTrueAction(2941, () => Press(U(ShiftMode))); + TriList.SetSigTrueAction(2942, () => Press(V(ShiftMode))); + TriList.SetSigTrueAction(2943, () => Press(W(ShiftMode))); + TriList.SetSigTrueAction(2944, () => Press(X(ShiftMode))); + TriList.SetSigTrueAction(2945, () => Press(Y(ShiftMode))); + TriList.SetSigTrueAction(2946, () => Press(Z(ShiftMode))); + TriList.SetSigTrueAction(2947, () => Press('.')); + TriList.SetSigTrueAction(2948, () => Press('@')); + TriList.SetSigTrueAction(2949, () => Press(' ')); + TriList.SetSigHeldAction(2950, 500, StartBackspaceRepeat, StopBackspaceRepeat, Backspace); + //TriList.SetSigTrueAction(2950, Backspace); + TriList.SetSigTrueAction(2951, Shift); + TriList.SetSigTrueAction(2952, NumShift); + TriList.SetSigTrueAction(2953, Clear); + TriList.SetSigTrueAction(2954, () => Press(DotComButtonString)); + + TriList.SetBool(GoButtonVisibleJoin, GoButtonVisible); + TriList.SetString(GoButtonTextJoin, GoButtonText); + TriList.SetBool(SecondaryButtonVisibleJoin, SecondaryButtonVisible); + TriList.SetString(SecondaryButtonTextJoin, SecondaryButtonText); + + TriList.SetBool(KeyboardVisible, true); + ShowKeys(); + IsVisible = true; + } + + /// + /// Hides the keyboard and disconnects ALL sig handlers from 2901 - 2969 + /// + public void Hide() + { + if (!IsVisible) + return; + + for (uint i = 2901; i < 2970; i++) + TriList.ClearBoolSigAction(i); + + // run attached actions + if(HideAction != null) + HideAction(); + + TriList.SetBool(KeyboardVisible, false); + IsVisible = false; + } + + /// + /// + /// + /// + public void Press(char c) + { + OnKeyPress(c.ToString()); + Output.Append(c); + OutputFeedback.FireUpdate(); + ResetShift(); + } + + /// + /// + /// + /// + public void Press(string s) + { + OnKeyPress(s); + Output.Append(s); + OutputFeedback.FireUpdate(); + ResetShift(); + } + + /// + /// + /// + public void EnableGoButton() + { + TriList.SetBool(GoButtonEnableJoin, true); + } + + /// + /// + /// + public void DisableGoButton() + { + TriList.SetBool(GoButtonEnableJoin, false); + } + + void ResetShift() + { + if (ShiftMode == 1) + { + ShiftMode = 0; + ShowKeys(); + } + else if (ShiftMode == 3) + { + ShiftMode = 2; + ShowKeys(); + } + } + + char A(int i) { return new char[] { 'a', 'A', '?', '?' }[i]; } + char B(int i) { return new char[] { 'b', 'B', ':', ':' }[i]; } + char C(int i) { return new char[] { 'c', 'C', '>', '>' }[i]; } + char D(int i) { return new char[] { 'd', 'D', '_', '_' }[i]; } + char E(int i) { return new char[] { 'e', 'E', '3', '#' }[i]; } + char F(int i) { return new char[] { 'f', 'F', '=', '=' }[i]; } + char G(int i) { return new char[] { 'g', 'G', '+', '+' }[i]; } + char H(int i) { return new char[] { 'h', 'H', '[', '[' }[i]; } + char I(int i) { return new char[] { 'i', 'I', '8', '*' }[i]; } + char J(int i) { return new char[] { 'j', 'J', ']', ']' }[i]; } + char K(int i) { return new char[] { 'k', 'K', '/', '/' }[i]; } + char L(int i) { return new char[] { 'l', 'L', '\\', '\\' }[i]; } + char M(int i) { return new char[] { 'm', 'M', '"', '"' }[i]; } + char N(int i) { return new char[] { 'n', 'N', '\'', '\'' }[i]; } + char O(int i) { return new char[] { 'o', 'O', '9', '(' }[i]; } + char P(int i) { return new char[] { 'p', 'P', '0', ')' }[i]; } + char Q(int i) { return new char[] { 'q', 'Q', '1', '!' }[i]; } + char R(int i) { return new char[] { 'r', 'R', '4', '$' }[i]; } + char S(int i) { return new char[] { 's', 'S', '-', '-' }[i]; } + char T(int i) { return new char[] { 't', 'T', '5', '%' }[i]; } + char U(int i) { return new char[] { 'u', 'U', '7', '&' }[i]; } + char V(int i) { return new char[] { 'v', 'V', ';', ';' }[i]; } + char W(int i) { return new char[] { 'w', 'W', '2', '@' }[i]; } + char X(int i) { return new char[] { 'x', 'X', '<', '<' }[i]; } + char Y(int i) { return new char[] { 'y', 'Y', '6', '^' }[i]; } + char Z(int i) { return new char[] { 'z', 'Z', ',', ',' }[i]; } + + /// + /// Does what it says + /// + void StartBackspaceRepeat() + { + if (BackspaceTimer == null) + { + BackspaceTimer = new CTimer(o => Backspace(), null, 0, 175); + } + } + + /// + /// Does what it says + /// + void StopBackspaceRepeat() + { + if (BackspaceTimer != null) + { + BackspaceTimer.Stop(); + BackspaceTimer = null; + } + } + + void Backspace() + { + OnKeyPress(KeyboardSpecialKey.Backspace); + + if (Output.Length > 0) + { + Output.Remove(Output.Length - 1, 1); + OutputFeedback.FireUpdate(); + } + } + + void Clear() + { + OnKeyPress(KeyboardSpecialKey.Clear); + + Output.Remove(0, Output.Length); + OutputFeedback.FireUpdate(); + } + + /* When in mode 0 (lowercase): + * shift button: up arrow 0 + * numShift button: 123/#$@#$ 0 + * + * - shift --> mode 1 + * - double-tap shift --> caps lock + * - numShift --> mode 2 + * + * mode 1 (uppercase) + * shift button: down arrow 1 + * numShift button: 123/##$# 0 + * + * - shift --> mode 0 + * - numShift --> mode 2 + * + * - Tapping any key will go back to mode 0 + * + * mode 2 (numbers-sym) + * Shift button: #$#$#$ 2 + * numShift: ABC 1 + * + * - shift --> mode 3 + * - double-tap shift --> caps lock + * - numShift --> mode 0 + * + * mode 3 (sym) + * Shift button: 123 3 + * numShift: ABC 1 + * + * - shift --> mode 2 + * - numShift --> mode 0 + * + * - Tapping any key will go back to mode 2 + */ + void Shift() + { + if (ShiftMode == 0) + ShiftMode = 1; + else if (ShiftMode == 1) + ShiftMode = 0; + else if (ShiftMode == 2) + ShiftMode = 3; + else + ShiftMode = 2; + + ShowKeys(); + } + + void NumShift() + { + if (ShiftMode == 0 || ShiftMode == 1) + ShiftMode = 2; + else if (ShiftMode == 2 || ShiftMode == 3) + ShiftMode = 0; + ShowKeys(); + } + + void ShowKeys() + { + TriList.SetString(2921, A(ShiftMode).ToString()); + TriList.SetString(2922, B(ShiftMode).ToString()); + TriList.SetString(2923, C(ShiftMode).ToString()); + TriList.SetString(2924, D(ShiftMode).ToString()); + TriList.SetString(2925, E(ShiftMode).ToString()); + TriList.SetString(2926, F(ShiftMode).ToString()); + TriList.SetString(2927, G(ShiftMode).ToString()); + TriList.SetString(2928, H(ShiftMode).ToString()); + TriList.SetString(2929, I(ShiftMode).ToString()); + TriList.SetString(2930, J(ShiftMode).ToString()); + TriList.SetString(2931, K(ShiftMode).ToString()); + TriList.SetString(2932, L(ShiftMode).ToString()); + TriList.SetString(2933, M(ShiftMode).ToString()); + TriList.SetString(2934, N(ShiftMode).ToString()); + TriList.SetString(2935, O(ShiftMode).ToString()); + TriList.SetString(2936, P(ShiftMode).ToString()); + TriList.SetString(2937, Q(ShiftMode).ToString()); + TriList.SetString(2938, R(ShiftMode).ToString()); + TriList.SetString(2939, S(ShiftMode).ToString()); + TriList.SetString(2940, T(ShiftMode).ToString()); + TriList.SetString(2941, U(ShiftMode).ToString()); + TriList.SetString(2942, V(ShiftMode).ToString()); + TriList.SetString(2943, W(ShiftMode).ToString()); + TriList.SetString(2944, X(ShiftMode).ToString()); + TriList.SetString(2945, Y(ShiftMode).ToString()); + TriList.SetString(2946, Z(ShiftMode).ToString()); + TriList.SetString(2954, DotComButtonString); + + TriList.SetUshort(2951, (ushort)ShiftMode); // 0 = up, 1 = down, 2 = #, 3 = 123 + TriList.SetUshort(2952, (ushort)(ShiftMode < 2 ? 0 : 1)); // 0 = #, 1 = abc + } + + /// + /// Event fire helper for text + /// + /// + void OnKeyPress(string text) + { + var handler = KeyPress; + if (handler != null) + KeyPress(this, new KeyboardControllerPressEventArgs(text)); + } + + /// + /// event helper for special keys + /// + /// + void OnKeyPress(KeyboardSpecialKey key) + { + var handler = KeyPress; + if (handler != null) + KeyPress(this, new KeyboardControllerPressEventArgs(key)); + } + + + /// + /// 2901 + /// + public const uint KeyboardVisible = 2901; + /// + /// 2902 + /// + public const uint ClosePressJoin = 2902; + /// + /// 2903 + /// + public const uint GoButtonPressJoin = 2903; + /// + /// 2903 + /// + public const uint GoButtonTextJoin = 2903; + /// + /// 2904 + /// + public const uint SecondaryButtonPressJoin = 2904; + /// + /// 2904 + /// + public const uint SecondaryButtonTextJoin = 2904; + /// + /// 2905 + /// + public const uint GoButtonVisibleJoin = 2905; + /// + /// 2906 + /// + public const uint SecondaryButtonVisibleJoin = 2906; + /// + /// 2907 + /// + public const uint GoButtonEnableJoin = 2907; + /// + /// 2910 + /// + public const uint ClearPressJoin = 2910; + /// + /// 2911 + /// + public const uint ClearVisibleJoin = 2911; + + } + + /// + /// + /// + public class KeyboardControllerPressEventArgs : EventArgs + { + public string Text { get; private set; } + public KeyboardSpecialKey SpecialKey { get; private set; } + + public KeyboardControllerPressEventArgs(string text) + { + Text = text; + } + + public KeyboardControllerPressEventArgs(KeyboardSpecialKey key) + { + SpecialKey = key; + } + } + + public enum KeyboardSpecialKey + { + None = 0, Backspace, Clear, GoButton, SecondaryButton + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/MOVED LargeTouchpanelControllerBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/MOVED LargeTouchpanelControllerBase.cs new file mode 100644 index 00000000..97c937b3 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/MOVED LargeTouchpanelControllerBase.cs @@ -0,0 +1,275 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; + +//using PepperDash.Core; + + +//namespace PepperDash.Essentials.Core +//{ +// /// +// /// +// /// +// public class LargeTouchpanelControllerBase : SmartGraphicsTouchpanelControllerBase +// { +// public string PresentationShareButtonInVideoText = "Share"; +// public string PresentationShareButtonNotInVideoText = "Presentation"; + +// SourceListSubpageReferenceList SourceSelectSRL; +// DevicePageControllerBase CurrentPresentationSourcePageController; + +// public LargeTouchpanelControllerBase(string key, string name, +// BasicTriListWithSmartObject triList, string sgdFilePath) +// : base(key, name, triList, sgdFilePath) +// { +// } + +// public override bool CustomActivate() +// { +// var baseSuccess = base.CustomActivate(); +// if (!baseSuccess) return false; + +// SourceSelectSRL = new SourceListSubpageReferenceList(this.TriList, n => +// { if (CurrentRoom != null) CurrentRoom.SelectSource(n); }); + +// var lm = Global.LicenseManager; +// if (lm != null) +// { +// lm.LicenseIsValid.LinkInputSig(TriList.BooleanInput[UiCue.ShowLicensed.Number]); +// //others +// } + +// // Temp things ----------------------------------------------------------------------- +// TriList.StringInput[UiCue.SplashMessage.Number].StringValue = SplashMessage; +// //------------------------------------------------------------------------------------ + +// // Initialize initial view +// ShowSplashOrMain(); +// return true; +// } + +// /// +// /// In Essentials, this should NEVER be called, since it's a one-room solution +// /// +// protected override void HideRoomUI() +// { +// // UI Cleanup here???? + +// //SwapAudioDeviceControls(CurrentRoom.CurrentAudioDevice, null); +// //CurrentRoom.AudioDeviceWillChange -= CurrentRoom_AudioDeviceWillChange; + +// CurrentRoom.IsCoolingDown.OutputChange -= CurrentRoom_IsCoolingDown_OutputChange; +// CurrentRoom.IsWarmingUp.OutputChange -= CurrentRoom_IsWarmingUp_OutputChange; + +// SourceSelectSRL.DetachFromCurrentRoom(); +// } + +// /// +// /// Ties this panel controller to the Room and gets updates. +// /// +// protected override void ShowRoomUI() +// { +// Debug.Console(1, this, "connecting to system '{0}'", CurrentRoom.Key); + +// TriList.StringInput[RoomCue.Name.Number].StringValue = CurrentRoom.Name; +// TriList.StringInput[RoomCue.Description.Number].StringValue = CurrentRoom.Description; + +// CurrentRoom.IsCoolingDown.OutputChange -= CurrentRoom_IsCoolingDown_OutputChange; +// CurrentRoom.IsWarmingUp.OutputChange -= CurrentRoom_IsWarmingUp_OutputChange; +// CurrentRoom.IsCoolingDown.OutputChange += CurrentRoom_IsCoolingDown_OutputChange; +// CurrentRoom.IsWarmingUp.OutputChange += CurrentRoom_IsWarmingUp_OutputChange; + +// SourceSelectSRL.AttachToRoom(CurrentRoom); +// } + +// void CurrentRoom_IsCoolingDown_OutputChange(object sender, EventArgs e) +// { +// Debug.Console(2, this, "Received room in cooldown={0}", CurrentRoom.IsCoolingDown.BoolValue); +// if (CurrentRoom.IsCoolingDown.BoolValue) // When entering cooldown +// { +// // Do we need to check for an already-running cooldown - like in the case of room switches? +// new ModalDialog(TriList).PresentModalTimerDialog(0, "Power Off", "Power", "Please wait, shutting down", +// "", "", CurrentRoom.CooldownTime, true, b => +// { +// ShowSplashOrMain(); +// }); +// } +// } + +// void CurrentRoom_IsWarmingUp_OutputChange(object sender, EventArgs e) +// { +// Debug.Console(2, this, "Received room in warmup={0}", CurrentRoom.IsWarmingUp.BoolValue); +// if (CurrentRoom.IsWarmingUp.BoolValue) // When entering warmup +// { +// // Do we need to check for an already-running cooldown - like in the case of room switches? +// new ModalDialog(TriList).PresentModalTimerDialog(0, "Power On", "Power", "Please wait, powering on", +// "", "", CurrentRoom.WarmupTime, false, b => +// { +// // Reveal sources - or has already been done behind modal +// }); +// } +// } + +// // Handler for source change events. +// void CurrentRoom_PresentationSourceChange(object sender, EssentialsRoomSourceChangeEventArgs args) +// { +// // Put away the old source and set up the new source. +// Debug.Console(2, this, "Received source change={0}", args.NewSource != null ? args.NewSource.Key : "none"); + +// // If we're in tech, don't switch screen modes. Add any other modes we may want to switch away from +// // inside the if below. +// if (MainMode == eMainModeType.Splash) +// setMainMode(eMainModeType.Presentation); +// SetControlSource(args.NewSource); +// } + +// //*********************************************************************** +// //** UI Manipulation +// //*********************************************************************** + +// /// +// /// Shows the splash page or the main presentation page, depending on config setting +// /// +// void ShowSplashOrMain() +// { +// if (UsesSplashPage) +// setMainMode(eMainModeType.Splash); +// else +// setMainMode(eMainModeType.Presentation); +// } + +// /// +// /// Switches between main modes +// /// +// void setMainMode(eMainModeType mode) +// { +// MainMode = mode; +// switch (mode) +// { +// case eMainModeType.Presentation: +// TriList.BooleanInput[UiCue.VisibleCommonFooter.Number].BoolValue = true; +// TriList.BooleanInput[UiCue.VisibleCommonHeader.Number].BoolValue = true; +// TriList.BooleanInput[UiCue.VisibleSplash.Number].BoolValue = false; +// TriList.BooleanInput[UiCue.VisiblePresentationSourceList.Number].BoolValue = true; +// ShowCurrentPresentationSourceUi(); +// break; +// case eMainModeType.Splash: +// TriList.BooleanInput[UiCue.VisibleCommonFooter.Number].BoolValue = false; +// TriList.BooleanInput[UiCue.VisibleCommonHeader.Number].BoolValue = false; +// TriList.BooleanInput[UiCue.VisiblePresentationSourceList.Number].BoolValue = false; +// TriList.BooleanInput[UiCue.VisibleSplash.Number].BoolValue = true; +// HideCurrentPresentationSourceUi(); +// break; +// case eMainModeType.Tech: +// new ModalDialog(TriList).PresentModalTimerDialog(1, "Tech page", "Info", +// "Tech page will be here soon!
I promise", +// "Bueno!", "", 0, false, null); +// MainMode = eMainModeType.Presentation; +// break; +// default: +// break; +// } +// } + +// void PowerOffWithConfirmPressed() +// { +// if (!CurrentRoom.RoomIsOn.BoolValue) return; +// // Timeout or button 1 press will shut down +// var modal = new ModalDialog(TriList); +// uint seconds = CurrentRoom.UnattendedShutdownTimeMs / 1000; +// var message = string.Format("Meeting will end in {0} seconds", seconds); +// modal.PresentModalTimerDialog(2, "End Meeting", "Info", message, +// "End Meeting Now", "Cancel", CurrentRoom.UnattendedShutdownTimeMs, true, +// but => { if (but != 2) CurrentRoom.RoomOff(); }); +// } + +// /// +// /// Reveals the basic UI for the current device +// /// +// protected override void ShowCurrentPresentationSourceUi() +// { +// if (MainMode == eMainModeType.Splash && CurrentRoom.RoomIsOn.BoolValue) +// setMainMode(eMainModeType.Presentation); + +// if (CurrentPresentationControlDevice == null) +// { +// // If system is off, do one thing + +// // Otherwise, do something else - shouldn't be in this condition + +// return; +// } + +// // If a controller is already loaded, use it +// if (LoadedPageControllers.ContainsKey(CurrentPresentationControlDevice)) +// CurrentPresentationSourcePageController = LoadedPageControllers[CurrentPresentationControlDevice]; +// else +// { +// // This is by no means optimal, but for now.... +// if (CurrentPresentationControlDevice.Type == PresentationSourceType.SetTopBox +// && CurrentPresentationControlDevice is IHasSetTopBoxProperties) +// CurrentPresentationSourcePageController = new PageControllerLargeSetTopBoxGeneric(TriList, +// CurrentPresentationControlDevice as IHasSetTopBoxProperties); + +// else if (CurrentPresentationControlDevice.Type == PresentationSourceType.Laptop) +// CurrentPresentationSourcePageController = new PageControllerLaptop(TriList); + +// // separate these... +// else if (CurrentPresentationControlDevice.Type == PresentationSourceType.Dvd) +// CurrentPresentationSourcePageController = +// new PageControllerLargeDvd(TriList, CurrentPresentationControlDevice as IHasCueActionList); + +// else +// CurrentPresentationSourcePageController = null; + +// // Save it. +// if (CurrentPresentationSourcePageController != null) +// LoadedPageControllers[CurrentPresentationControlDevice] = CurrentPresentationSourcePageController; +// } + +// if (CurrentPresentationSourcePageController != null) +// CurrentPresentationSourcePageController.SetVisible(true); +// } + +// protected override void HideCurrentPresentationSourceUi() +// { +// if (CurrentPresentationControlDevice != null && CurrentPresentationSourcePageController != null) +// CurrentPresentationSourcePageController.SetVisible(false); +// } + + + +// void ShowHelp() +// { +// new ModalDialog(TriList).PresentModalTimerDialog(1, "Help", "Help", CurrentRoom.HelpMessage, +// "OK", "", 0, false, null); +// } + +// protected void ListSmartObjects() +// { +// Debug.Console(0, this, "Smart objects IDs:"); +// var list = TriList.SmartObjects.OrderBy(s => s.Key); +// foreach (var kvp in list) +// Debug.Console(0, " {0}", kvp.Key); +// } + +// public override List FunctionList +// { +// get +// { +// return new List +// { +// new BoolCueActionPair(UiCue.PressSplash, b => { if(!b) setMainMode(eMainModeType.Presentation); }), +// new BoolCueActionPair(UiCue.PressRoomOffWithConfirm, b => { if(!b) PowerOffWithConfirmPressed(); }), +// new BoolCueActionPair(UiCue.PressModePresentationShare, b => { if(!b) setMainMode(eMainModeType.Presentation); }), +// new BoolCueActionPair(UiCue.PressHelp, b => { if(!b) ShowHelp(); }), +// new BoolCueActionPair(UiCue.PressSettings, b => { if(!b) setMainMode(eMainModeType.Tech); }), +// }; +// } +// } +// //#endregion +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/MOVED UIControllers/DevicePageControllerBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/MOVED UIControllers/DevicePageControllerBase.cs new file mode 100644 index 00000000..334050e0 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/MOVED UIControllers/DevicePageControllerBase.cs @@ -0,0 +1,244 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; + +//using PepperDash.Essentials.Core.Presets; + +//using PepperDash.Core; + + +//namespace PepperDash.Essentials.Core +//{ +// /// +// /// +// /// +// public abstract class DevicePageControllerBase +// { + +// protected BasicTriListWithSmartObject TriList; +// protected List FixedObjectSigs; + +// public DevicePageControllerBase(BasicTriListWithSmartObject triList) +// { +// TriList = triList; +// } + +// public void SetVisible(bool state) +// { +// foreach (var sig in FixedObjectSigs) +// { +// Debug.Console(2, "set visible {0}={1}", sig.Number, state); +// sig.BoolValue = state; +// } +// CustomSetVisible(state); +// } + +// /// +// /// Add any specialized show/hide logic here - beyond FixedObjectSigs. Overriding +// /// methods do not need to call this base method +// /// +// protected virtual void CustomSetVisible(bool state) +// { +// } +// } + +// //public class InterlockedDevicePageController +// //{ +// // public List ObjectBoolJoins { get; set; } +// // public uint DefaultJoin { get; set; } +// //} + + + + +// ///// +// ///// +// ///// +// //public interface IHasSetTopBoxProperties +// //{ +// // bool HasDpad { get; } +// // bool HasPreset { get; } +// // bool HasDvr { get; } +// // bool HasNumbers { get; } +// // DevicePresetsModel PresetsModel { get; } +// // void LoadPresets(string filePath); +// //} + +// public class PageControllerLaptop : DevicePageControllerBase +// { +// public PageControllerLaptop(BasicTriListWithSmartObject tl) +// : base(tl) +// { +// FixedObjectSigs = new List +// { +// tl.BooleanInput[10092], // well +// tl.BooleanInput[11001] // Laptop info +// }; +// } +// } + +// ///// +// ///// +// ///// +// //public class PageControllerLargeDvd : DevicePageControllerBase +// //{ +// // IHasCueActionList Device; + +// // public PageControllerLargeDvd(BasicTriListWithSmartObject tl, IHasCueActionList device) +// // : base(tl) +// // { + +// // Device = device; +// // FixedObjectSigs = new List +// // { +// // tl.BooleanInput[10093], // well +// // tl.BooleanInput[10411], // DVD Dpad +// // tl.BooleanInput[10412] // everything else +// // }; +// // } + +// // protected override void CustomSetVisible(bool state) +// // { +// // // Hook up smart objects if applicable +// // if (Device != null) +// // { +// // var uos = (Device as IHasCueActionList).CueActionList; +// // SmartObjectHelper.LinkDpadWithUserObjects(TriList, 10411, uos, state); +// // } +// // } +// //} + + +// /// +// /// +// /// +// //public class PageControllerLargeSetTopBoxGeneric : DevicePageControllerBase +// //{ +// // // To-DO: Add properties for component subpage names. DpadPos1, DpadPos2... +// // // Derived classes can then insert special subpages for variations on given +// // // device types. Like DirecTV vs Comcast + +// // public uint DpadSmartObjectId { get; set; } +// // public uint NumberPadSmartObjectId { get; set; } +// // public uint PresetsSmartObjectId { get; set; } +// // public uint Position5TabsId { get; set; } + +// // IHasSetTopBoxProperties Device; +// // DevicePresetsView PresetsView; + + +// // bool ShowPosition5Tabs; +// // uint CurrentVisiblePosition5Item = 1; +// // Dictionary Position5SubpageJoins = new Dictionary +// // { +// // { 1, 10053 }, +// // { 2, 10054 } +// // }; + +// // public PageControllerLargeSetTopBoxGeneric(BasicTriListWithSmartObject tl, IHasSetTopBoxProperties device) +// // : base(tl) +// // { +// // Device = device; +// // DpadSmartObjectId = 10011; +// // NumberPadSmartObjectId = 10014; +// // PresetsSmartObjectId = 10012; +// // Position5TabsId = 10081; + +// // bool dpad = device.HasDpad; +// // bool preset = device.HasPreset; +// // bool dvr = device.HasDvr; +// // bool numbers = device.HasNumbers; +// // uint[] joins = null; + +// // if (dpad && !preset && !dvr && !numbers) joins = new uint[] { 10031, 10091 }; +// // else if (!dpad && preset && !dvr && !numbers) joins = new uint[] { 10032, 10091 }; +// // else if (!dpad && !preset && dvr && !numbers) joins = new uint[] { 10033, 10091 }; +// // else if (!dpad && !preset && !dvr && numbers) joins = new uint[] { 10034, 10091 }; + +// // else if (dpad && preset && !dvr && !numbers) joins = new uint[] { 10042, 10021, 10092 }; +// // else if (dpad && !preset && dvr && !numbers) joins = new uint[] { 10043, 10021, 10092 }; +// // else if (dpad && !preset && !dvr && numbers) joins = new uint[] { 10044, 10021, 10092 }; +// // else if (!dpad && preset && dvr && !numbers) joins = new uint[] { 10043, 10022, 10092 }; +// // else if (!dpad && preset && !dvr && numbers) joins = new uint[] { 10044, 10022, 10092 }; +// // else if (!dpad && !preset && dvr && numbers) joins = new uint[] { 10044, 10023, 10092 }; + +// // else if (dpad && preset && dvr && !numbers) joins = new uint[] { 10053, 10032, 10011, 10093 }; +// // else if (dpad && preset && !dvr && numbers) joins = new uint[] { 10054, 10032, 10011, 10093 }; +// // else if (dpad && !preset && dvr && numbers) joins = new uint[] { 10054, 10033, 10011, 10093 }; +// // else if (!dpad && preset && dvr && numbers) joins = new uint[] { 10054, 10033, 10012, 10093 }; + +// // else if (dpad && preset && dvr && numbers) +// // { +// // joins = new uint[] { 10081, 10032, 10011, 10093 }; // special case +// // ShowPosition5Tabs = true; +// // } +// // // Project the joins into corresponding sigs. +// // FixedObjectSigs = joins.Select(u => TriList.BooleanInput[u]).ToList(); + +// // // Build presets +// // if (device.HasPreset) +// // { +// // PresetsView = new DevicePresetsView(tl, device.PresetsModel); +// // } + + +// // } + +// // protected override void CustomSetVisible(bool state) +// // { +// // if (ShowPosition5Tabs) +// // { +// // // Show selected tab +// // TriList.BooleanInput[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = state; + +// // var tabSo = TriList.SmartObjects[Position5TabsId]; +// // if (state) // Link up the tab object + +// // { +// // tabSo.BooleanOutput["Tab Button 1 Press"].UserObject = new BoolCueActionPair(b => ShowTab(1)); +// // tabSo.BooleanOutput["Tab Button 2 Press"].UserObject = new BoolCueActionPair(b => ShowTab(2)); +// // } +// // else // Disco tab object +// // { +// // tabSo.BooleanOutput["Tab Button 1 Press"].UserObject = null; +// // tabSo.BooleanOutput["Tab Button 2 Press"].UserObject = null; +// // } +// // } + +// // // Hook up smart objects if applicable +// // if (Device is IHasCueActionList) +// // { +// // var uos = (Device as IHasCueActionList).CueActionList; +// // SmartObjectHelper.LinkDpadWithUserObjects(TriList, DpadSmartObjectId, uos, state); +// // SmartObjectHelper.LinkNumpadWithUserObjects(TriList, NumberPadSmartObjectId, +// // uos, CommonBoolCue.Dash, CommonBoolCue.Last, state); +// // } + + +// // // Link, unlink presets +// // if (Device.HasPreset && state) +// // PresetsView.Attach(); +// // else if (Device.HasPreset && !state) +// // PresetsView.Detach(); +// // } + +// // void ShowTab(uint number) +// // { +// // // Ignore re-presses +// // if (CurrentVisiblePosition5Item == number) return; +// // // Swap subpage +// // var bi = TriList.BooleanInput; +// // if(CurrentVisiblePosition5Item > 0) +// // bi[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = false; +// // CurrentVisiblePosition5Item = number; +// // bi[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = true; + +// // // Show feedback on buttons +// // } + +// //} +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/ModalDialog.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/ModalDialog.cs new file mode 100644 index 00000000..3b2ff89b --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/ModalDialog.cs @@ -0,0 +1,211 @@ +using System; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DeviceSupport; +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + public class ModalDialog + { + /// + /// Bool press 3991 + /// + public const uint Button1Join = 3991; + /// + /// Bool press 3992 + /// + public const uint Button2Join = 3992; + /// + /// 3993 + /// + public const uint CancelButtonJoin = 3993; + /// + ///For visibility of single button. Bool feedback 3994 + /// + public const uint OneButtonVisibleJoin = 3994; + /// + /// For visibility of two buttons. Bool feedback 3995. + /// + public const uint TwoButtonVisibleJoin = 3995; + /// + /// Shows the timer guage if in use. Bool feedback 3996 + /// + public const uint TimerVisibleJoin = 3996; + /// + /// Visibility join to show "X" button 3997 + /// + public const uint CancelVisibleJoin = 3997; + /// + /// Shows the modal subpage. Boolean feeback join 3999 + /// + public const uint ModalVisibleJoin = 3999; + + /// + /// The seconds value of the countdown timer. Ushort join 3991 + /// + //public const uint TimerSecondsJoin = 3991; + /// + /// The full ushort value of the countdown timer for a gauge. Ushort join 3992 + /// + public const uint TimerGaugeJoin = 3992; + + /// + /// Text on button one. String join 3991 + /// + public const uint Button1TextJoin = 3991; + /// + /// Text on button two. String join 3992 + /// + public const uint Button2TextJoin = 3992; + /// + /// Message text. String join 3994 + /// + public const uint MessageTextJoin = 3994; + /// + /// Title text. String join 3995 + /// + public const uint TitleTextJoin = 3995; + /// + /// Icon name. String join 3996 + /// + public const uint IconNameJoin = 3996; + + /// + /// Returns true when modal is showing + /// + public bool ModalIsVisible + { + get { return TriList.BooleanInput[ModalVisibleJoin].BoolValue; } + } + + /// + /// + /// + public bool CanCancel { get; private set; } + + + BasicTriList TriList; + + Action ModalCompleteAction; + + static object CompleteActionLock = new object(); + + /// + /// Creates a new modal to be shown on provided TriList + /// + /// + public ModalDialog(BasicTriList triList) + { + TriList = triList; + // Attach actions to buttons + + triList.SetSigFalseAction(Button1Join, () => OnModalComplete(1)); + triList.SetSigFalseAction(Button2Join, () => OnModalComplete(2)); + triList.SetSigFalseAction(CancelButtonJoin, () => { if (CanCancel) CancelDialog(); }); + CanCancel = true; + } + + /// + /// Shows the dialog + /// + /// Number of buttons to show. 0, 1, 2 + /// The amount of time to show the dialog. Use 0 for no timeout. + /// If the progress bar gauge needs to count down instead of up + /// The action to run when the dialog is dismissed. Parameter will be 1 or 2 if button pressed, or 0 if dialog times out + /// True when modal is created. + public bool PresentModalDialog(uint numberOfButtons, string title, string iconName, + string message, string button1Text, + string button2Text, bool showGauge, bool showCancel, Action completeAction) + { + // Don't reset dialog if visible now + if (!ModalIsVisible) + { + ModalCompleteAction = completeAction; + TriList.StringInput[TitleTextJoin].StringValue = title; + if (string.IsNullOrEmpty(iconName)) iconName = "Blank"; + TriList.StringInput[IconNameJoin].StringValue = iconName; + TriList.StringInput[MessageTextJoin].StringValue = message; + if (numberOfButtons == 0) + { + // Show no buttons + TriList.BooleanInput[OneButtonVisibleJoin].BoolValue = false; + TriList.BooleanInput[TwoButtonVisibleJoin].BoolValue = false; + } + else if (numberOfButtons == 1) + { + // Show one button + TriList.BooleanInput[OneButtonVisibleJoin].BoolValue = true; + TriList.BooleanInput[TwoButtonVisibleJoin].BoolValue = false; + TriList.StringInput[Button1TextJoin].StringValue = button1Text; + } + else if (numberOfButtons == 2) + { + // Show two + TriList.BooleanInput[OneButtonVisibleJoin].BoolValue = false; + TriList.BooleanInput[TwoButtonVisibleJoin].BoolValue = true; + TriList.StringInput[Button1TextJoin].StringValue = button1Text; + TriList.StringInput[Button2TextJoin].StringValue = button2Text; + } + // Show/hide guage + TriList.BooleanInput[TimerVisibleJoin].BoolValue = showGauge; + + CanCancel = showCancel; + TriList.BooleanInput[CancelVisibleJoin].BoolValue = showCancel; + + //Reveal and activate + TriList.BooleanInput[ModalVisibleJoin].BoolValue = true; + + WakePanel(); + + return true; + } + + return false; + } + + /// + /// Wakes the panel by turning on the backlight if off + /// + public void WakePanel() + { + try + { + var panel = TriList as TswFt5Button; + + if (panel != null && panel.ExtenderSystemReservedSigs.BacklightOffFeedback.BoolValue) + panel.ExtenderSystemReservedSigs.BacklightOn(); + } + catch + { + Debug.Console(1, "Error Waking Panel. Maybe testing with Xpanel?"); + } + } + + /// + /// Hide dialog from elsewhere, fires CompleteAction + /// + public void CancelDialog() + { + OnModalComplete(0); + } + + /// + /// Hides dialog. Fires no action + /// + public void HideDialog() + { + TriList.BooleanInput[ModalVisibleJoin].BoolValue = false; + } + + // When the modal is cleared or times out, clean up the various bits + void OnModalComplete(uint buttonNum) + { + TriList.BooleanInput[ModalVisibleJoin].BoolValue = false; + + var action = ModalCompleteAction; + if (action != null) + action(buttonNum); + } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/REMOVE Tsr302Controller.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/REMOVE Tsr302Controller.cs new file mode 100644 index 00000000..13cfabcc --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/REMOVE Tsr302Controller.cs @@ -0,0 +1,135 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +//using Crestron.SimplSharpPro.UI; + +//using PepperDash.Core; + + +//namespace PepperDash.Essentials.Core +//{ +//[Obsolete("Replaced, initially with CrestronTsr302Controller in Resissentials")] +// public class Tsr302Controller : SmartGraphicsTouchpanelControllerBase +// { +// //public override List FunctionList +// //{ +// // get +// // { +// // return new List +// // { + +// // }; +// // } +// //} + +// public Tsr302 Remote { get; private set; } + +// SourceListSubpageReferenceList SourceSelectSRL; +// DevicePageControllerBase CurrentPresentationSourcePageController; +// CTimer VolumeFeedbackTimer; + + +// public Tsr302Controller(string key, string name, Tsr302 device, string sgdFilePath) : +// base(key, name, device, sgdFilePath) +// { +// // Base takes care of TriList +// Remote = device; +// Remote.Home.UserObject = new BoolCueActionPair(b => { if (!b) PressHome(); }); +// Remote.VolumeUp.UserObject = new BoolCueActionPair(b => { if (!b) PressHome(); }); +// Remote.ButtonStateChange += Remote_ButtonStateChange; +// } + +// public override bool CustomActivate() +// { +// var baseSuccess = base.CustomActivate(); +// if (!baseSuccess) return false; + +// SourceSelectSRL = new SourceListSubpageReferenceList(this.TriList, n => +// { if (CurrentRoom != null) CurrentRoom.SelectSource(n); }); + + +// return true; +// } + +// protected override void SwapAudioDeviceControls(IVolumeFunctions oldDev, IVolumeFunctions newDev) +// { +// // stop presses +// if (oldDev != null) +// { +// ReleaseAudioPresses(); +// if (oldDev is IVolumeTwoWay) +// { +// (newDev as IVolumeTwoWay).VolumeLevelFeedback.OutputChange -= VolumeLevelOutput_OutputChange; +// (oldDev as IVolumeTwoWay).VolumeLevelFeedback +// .UnlinkInputSig(TriList.UShortInput[CommonIntCue.MainVolumeLevel.Number]); +// } +// } + +// if (newDev != null) +// { +// Remote.VolumeDown.UserObject = newDev.VolumeDownCueActionPair; +// Remote.VolumeUp.UserObject = newDev.VolumeUpCueActionPair; +// Remote.Mute.UserObject = newDev.MuteToggleCueActionPair; +// if (newDev is IVolumeTwoWay) +// { +// var vOut = (newDev as IVolumeTwoWay).VolumeLevelFeedback; +// vOut.OutputChange += VolumeLevelOutput_OutputChange; +// TriList.UShortInput[CommonIntCue.MainVolumeLevel.Number].UShortValue = vOut.UShortValue; +// } +// } +// else +// { +// Remote.VolumeDown.UserObject = null; +// Remote.VolumeUp.UserObject = null; +// Remote.Mute.UserObject = null; +// } + +// base.SwapAudioDeviceControls(oldDev, newDev); +// } + +// void PressHome() +// { + +// } + +// void VolumeLevelOutput_OutputChange(object sender, EventArgs e) +// { +// // Set level and show popup on timer +// TriList.UShortInput[CommonIntCue.MainVolumeLevel.Number].UShortValue = +// (sender as IntFeedback).UShortValue; + +// if (VolumeFeedbackTimer == null) +// { +// TriList.BooleanInput[CommonBoolCue.ShowVolumeSlider.Number].BoolValue = true; +// VolumeFeedbackTimer = new CTimer(o => { +// TriList.BooleanInput[CommonBoolCue.ShowVolumeSlider.Number].BoolValue = false; +// }, 1000); +// } + +// } + +// void ReleaseAudioPresses() +// { +// if (Remote.VolumeDown.UserObject is BoolCueActionPair && Remote.VolumeDown.State == eButtonState.Pressed) +// (Remote.VolumeDown.UserObject as BoolCueActionPair).Invoke(false); +// if (Remote.VolumeUp.UserObject is BoolCueActionPair && Remote.VolumeUp.State == eButtonState.Pressed) +// (Remote.VolumeUp.UserObject as BoolCueActionPair).Invoke(false); +// if (Remote.Mute.UserObject is BoolCueActionPair && Remote.Mute.State == eButtonState.Pressed) +// (Remote.Mute.UserObject as BoolCueActionPair).Invoke(false); +// } + +// /// +// /// Handler. Run UO's stored in buttons +// /// +// void Remote_ButtonStateChange(GenericBase device, ButtonEventArgs args) +// { +// Debug.Console(2, this, "{0}={1}", args.Button.Name, args.Button.State); +// var uo = args.Button.UserObject as BoolCueActionPair; +// if (uo != null) +// uo.Invoke(args.NewButtonState == eButtonState.Pressed); +// } +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/SmartGraphicsTouchpanelControllerBase.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/SmartGraphicsTouchpanelControllerBase.cs new file mode 100644 index 00000000..42c365d1 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/SmartGraphicsTouchpanelControllerBase.cs @@ -0,0 +1,309 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; + + +//namespace PepperDash.Essentials.Core +//{ +// public abstract class SmartGraphicsTouchpanelControllerBase : CrestronGenericBaseDevice +// { +// public BasicTriListWithSmartObject TriList { get; protected set; } +// public bool UsesSplashPage { get; set; } +// public string SplashMessage { get; set; } +// public bool ShowDate +// { +// set { TriList.BooleanInput[UiCue.ShowDate.Number].BoolValue = value; } +// } +// public bool ShowTime +// { +// set { TriList.BooleanInput[UiCue.ShowTime.Number].BoolValue = value; } +// } + +// public abstract List FunctionList { get; } + + +// protected eMainModeType MainMode; +// protected IPresentationSource CurrentPresentationControlDevice; + +// /// +// /// Defines the signal offset for the presentation device. Defaults to 100 +// /// +// public uint PresentationControlDeviceJoinOffset { get { return 100; } } + +// public enum eMainModeType +// { +// Presentation, Splash, Tech +// } + +// protected string SgdFilePath; +// public EssentialsRoom CurrentRoom { get; protected set; } +// protected Dictionary LoadedPageControllers +// = new Dictionary(); + +// static object RoomChangeLock = new object(); + +// /// +// /// Constructor +// /// +// public SmartGraphicsTouchpanelControllerBase(string key, string name, BasicTriListWithSmartObject triList, +// string sgdFilePath) +// : base(key, name, triList) +// { +// TriList = triList; +// if (string.IsNullOrEmpty(key)) throw new ArgumentNullException("key"); +// if (string.IsNullOrEmpty(sgdFilePath)) throw new ArgumentNullException("sgdFilePath"); +// SgdFilePath = sgdFilePath; +// TriList.LoadSmartObjects(SgdFilePath); +// UsesSplashPage = true; +// SplashMessage = "Welcome"; +// TriList.SigChange += Tsw_AnySigChange; +// foreach (var kvp in TriList.SmartObjects) +// kvp.Value.SigChange += this.Tsw_AnySigChange; +// } + +// public override bool CustomActivate() +// { +// var baseSuccess = base.CustomActivate(); +// if (!baseSuccess) return false; + +// // Wiring up the buttons with UOs +// foreach (var uo in this.FunctionList) +// { +// if (uo.Cue.Number == 0) continue; +// if (uo is BoolCueActionPair) +// TriList.BooleanOutput[uo.Cue.Number].UserObject = uo; +// else if (uo is UShortCueActionPair) +// TriList.UShortOutput[uo.Cue.Number].UserObject = uo; +// else if (uo is StringCueActionPair) +// TriList.StringOutput[uo.Cue.Number].UserObject = uo; +// } + +// return true; +// } + +// //public void SetCurrentRoom(EssentialsRoom room) +// //{ +// // if (CurrentRoom != null) +// // HideRoomUI(); +// // CurrentRoom = room; +// // ShowRoomUI(); +// //} + +// /// +// /// +// /// +// /// +// public void SetCurrentRoom(EssentialsRoom room) +// { +// if (CurrentRoom == room) return; + +// IVolumeFunctions oldAudio = null; +// //Disconnect current room and audio device +// if (CurrentRoom != null) +// { +// HideRoomUI(); +// CurrentRoom.AudioDeviceWillChange -= CurrentRoom_AudioDeviceWillChange; +// CurrentRoom.PresentationSourceChange -= CurrentRoom_PresentationSourceChange; +// oldAudio = CurrentRoom.CurrentAudioDevice; +// } + +// CurrentRoom = room; +// IVolumeFunctions newAudio = null; +// if (CurrentRoom != null) +// { +// CurrentRoom.AudioDeviceWillChange += this.CurrentRoom_AudioDeviceWillChange; +// CurrentRoom.PresentationSourceChange += this.CurrentRoom_PresentationSourceChange; +// SetControlSource(CurrentRoom.CurrentPresentationSource); +// newAudio = CurrentRoom.CurrentAudioDevice; +// ShowRoomUI(); +// } + +// SwapAudioDeviceControls(oldAudio, newAudio); +// } + +// /// +// /// Detaches and attaches an IVolumeFunctions device to the appropriate TP TriList signals. +// /// This will also add IVolumeNumeric if the device implements it. +// /// Overriding classes should call this. Overriding classes are responsible for +// /// linking up to hard keys, etc. +// /// +// /// May be null +// /// May be null +// protected virtual void SwapAudioDeviceControls(IVolumeFunctions oldDev, IVolumeFunctions newDev) +// { +// // Disconnect +// if (oldDev != null) +// { +// TriList.BooleanOutput[CommonBoolCue.VolumeDown.Number].UserObject = null; +// TriList.BooleanOutput[CommonBoolCue.VolumeUp.Number].UserObject = null; +// TriList.BooleanOutput[CommonBoolCue.MuteToggle.Number].UserObject = null; +// TriList.BooleanInput[CommonBoolCue.ShowVolumeButtons.Number].BoolValue = false; +// TriList.BooleanInput[CommonBoolCue.ShowVolumeSlider.Number].BoolValue = false; +// if (oldDev is IVolumeTwoWay) +// { +// TriList.UShortOutput[CommonIntCue.MainVolumeLevel.Number].UserObject = null; +// (oldDev as IVolumeTwoWay).IsMutedFeedback +// .UnlinkInputSig(TriList.BooleanInput[CommonBoolCue.MuteOn.Number]); +// (oldDev as IVolumeTwoWay).VolumeLevelFeedback +// .UnlinkInputSig(TriList.UShortInput[CommonIntCue.MainVolumeLevel.Number]); +// } +// } +// if (newDev != null) +// { +// TriList.BooleanInput[CommonBoolCue.ShowVolumeSlider.Number].BoolValue = true; +// TriList.BooleanOutput[CommonBoolCue.VolumeDown.Number].UserObject = newDev.VolumeDownCueActionPair; +// TriList.BooleanOutput[CommonBoolCue.VolumeUp.Number].UserObject = newDev.VolumeUpCueActionPair; +// TriList.BooleanOutput[CommonBoolCue.MuteToggle.Number].UserObject = newDev.MuteToggleCueActionPair; +// if (newDev is IVolumeTwoWay) +// { +// TriList.BooleanInput[CommonBoolCue.ShowVolumeSlider.Number].BoolValue = true; +// var numDev = newDev as IVolumeTwoWay; +// TriList.UShortOutput[CommonIntCue.MainVolumeLevel.Number].UserObject = numDev.VolumeLevelCueActionPair; +// numDev.VolumeLevelFeedback +// .LinkInputSig(TriList.UShortInput[CommonIntCue.MainVolumeLevel.Number]); +// } +// } +// } + + +// /// +// /// Does nothing. Override to add functionality when calling SetCurrentRoom +// /// +// protected virtual void HideRoomUI() { } + +// /// +// /// Does nothing. Override to add functionality when calling SetCurrentRoom +// /// +// protected virtual void ShowRoomUI() { } + +// /// +// /// Sets up the current presentation device and updates statuses if the device is capable. +// /// +// protected void SetControlSource(IPresentationSource newSource) +// { +// if (CurrentPresentationControlDevice != null) +// { +// HideCurrentPresentationSourceUi(); + +// // Unhook presses and things +// if (CurrentPresentationControlDevice is IHasCueActionList) +// { +// foreach (var uo in (CurrentPresentationControlDevice as IHasCueActionList).CueActionList) +// { +// if (uo.Cue.Number == 0) continue; +// if (uo is BoolCueActionPair) +// { +// var bSig = TriList.BooleanOutput[uo.Cue.Number]; +// // Disconnection should also clear bool sigs in case they are pressed and +// // might be orphaned +// if (bSig.BoolValue) +// (bSig.UserObject as BoolCueActionPair).Invoke(false); +// bSig.UserObject = null; +// } +// else if (uo is UShortCueActionPair) +// TriList.UShortOutput[uo.Cue.Number].UserObject = null; +// else if (uo is StringCueActionPair) +// TriList.StringOutput[uo.Cue.Number].UserObject = null; +// } +// } +// // unhook outputs +// if (CurrentPresentationControlDevice is IHasFeedback) +// { +// foreach (var output in (CurrentPresentationControlDevice as IHasFeedback).Feedbacks) +// { +// if (output.Cue.Number == 0) continue; +// if (output is BoolFeedback) +// (output as BoolFeedback).UnlinkInputSig(TriList.BooleanInput[output.Cue.Number]); +// else if (output is IntFeedback) +// (output as IntFeedback).UnlinkInputSig(TriList.UShortInput[output.Cue.Number]); +// else if (output is StringFeedback) +// (output as StringFeedback).UnlinkInputSig(TriList.StringInput[output.Cue.Number]); +// } +// } +// } +// CurrentPresentationControlDevice = newSource; +// //connect presses and things +// if (newSource is IHasCueActionList) // This has functions, get 'em +// { +// foreach (var ao in (newSource as IHasCueActionList).CueActionList) +// { +// if (ao.Cue.Number == 0) continue; +// if (ao is BoolCueActionPair) +// TriList.BooleanOutput[ao.Cue.Number].UserObject = ao; +// else if (ao is UShortCueActionPair) +// TriList.UShortOutput[ao.Cue.Number].UserObject = ao; +// else if (ao is StringCueActionPair) +// TriList.StringOutput[ao.Cue.Number].UserObject = ao; +// } +// } +// // connect outputs (addInputSig should update sig) +// if (CurrentPresentationControlDevice is IHasFeedback) +// { +// foreach (var output in (CurrentPresentationControlDevice as IHasFeedback).Feedbacks) +// { +// if (output.Cue.Number == 0) continue; +// if (output is BoolFeedback) +// (output as BoolFeedback).LinkInputSig(TriList.BooleanInput[output.Cue.Number]); +// else if (output is IntFeedback) +// (output as IntFeedback).LinkInputSig(TriList.UShortInput[output.Cue.Number]); +// else if (output is StringFeedback) +// (output as StringFeedback).LinkInputSig(TriList.StringInput[output.Cue.Number]); +// } +// } +// ShowCurrentPresentationSourceUi(); +// } + +// /// +// /// Reveals the basic UI for the current device +// /// +// protected virtual void ShowCurrentPresentationSourceUi() +// { +// } + +// /// +// /// Hides the UI for the current device and calls for a feedback signal cleanup +// /// +// protected virtual void HideCurrentPresentationSourceUi() +// { +// } + + +// /// +// /// +// /// +// void CurrentRoom_PresentationSourceChange(object sender, EssentialsRoomSourceChangeEventArgs args) +// { +// SetControlSource(args.NewSource); +// } + + +// /// +// /// +// /// +// void CurrentRoom_AudioDeviceWillChange(object sender, EssentialsRoomAudioDeviceChangeEventArgs e) +// { +// SwapAudioDeviceControls(e.OldDevice, e.NewDevice); +// } + + + +// /// +// /// Panel event handler +// /// +// void Tsw_AnySigChange(object currentDevice, SigEventArgs args) +// { +// // plugged in commands +// object uo = args.Sig.UserObject; + +// if (uo is Action) +// (uo as Action)(args.Sig.BoolValue); +// else if (uo is Action) +// (uo as Action)(args.Sig.UShortValue); +// else if (uo is Action) +// (uo as Action)(args.Sig.StringValue); +// } +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/TriListExtensions.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/TriListExtensions.cs new file mode 100644 index 00000000..b282d475 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/TriListExtensions.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Core +{ + /// + /// Extensions used for more-clear attachment of Actions to user objects on sigs + /// + public static class SigAndTriListExtensions + { + /// + /// Attaches Action to Sig's user object and returns the same Sig. This provides no protection + /// from null sigs + /// + /// The BoolOutputSig to attach the Action to + /// An action to run when sig is pressed and when released + /// The Sig, sig + public static BoolOutputSig SetBoolSigAction(this BoolOutputSig sig, Action a) + { + sig.UserObject = a; + return sig; + } + + /// + /// Attaches Action to Sig's user object and returns the same Sig. + /// + /// + /// + /// + /// + public static BoolOutputSig SetBoolSigAction(this BasicTriList tl, uint sigNum, Action a) + { + return tl.BooleanOutput[sigNum].SetBoolSigAction(a); + } + + public static BoolOutputSig SetSigTrueAction(this BasicTriList tl, uint sigNum, Action a) + { + return tl.BooleanOutput[sigNum].SetBoolSigAction(b => { if(b) a(); }); + } + + /// + /// Attaches a void Action to a TriList's output sig's UserObject, to be run on release + /// + /// The sig + public static BoolOutputSig SetSigFalseAction(this BasicTriList tl, uint sigNum, Action a) + { + return tl.BooleanOutput[sigNum].SetBoolSigAction(b => { if (!b) a(); }); + } + + /// + /// Attaches a void Action to an output sig's UserObject, to be run on release + /// + /// The Sig + public static BoolOutputSig SetSigFalseAction(this BoolOutputSig sig, Action a) + { + return sig.SetBoolSigAction(b => { if (!b) a(); }); + } + + /// + /// Sets an action to a held sig + /// + /// The sig + public static BoolOutputSig SetSigHeldAction(this BasicTriList tl, uint sigNum, uint heldMs, Action heldAction) + { + return SetSigHeldAction(tl, sigNum, heldMs, heldAction, null); + } + + /// + /// Sets an action to a held sig as well as a released-without-hold action + /// + /// + public static BoolOutputSig SetSigHeldAction(this BoolOutputSig sig, uint heldMs, Action heldAction, Action holdReleasedAction, Action releaseAction) + { + CTimer heldTimer = null; + bool wasHeld = false; + return sig.SetBoolSigAction(press => + { + if (press) + { + wasHeld = false; + // Could insert a pressed action here + heldTimer = new CTimer(o => + { + // if still held and there's an action + if (sig.BoolValue && heldAction != null) + { + wasHeld = true; + // Hold action here + heldAction(); + } + }, heldMs); + } + else if (!press && !wasHeld) // released, no hold + { + heldTimer.Stop(); + if (releaseAction != null) + releaseAction(); + } + else // !press && wasHeld // released after held + { + heldTimer.Stop(); + if (holdReleasedAction != null) + holdReleasedAction(); + } + }); + + } + + /// + /// Sets an action to a held sig as well as a released-without-hold action + /// + /// The sig + public static BoolOutputSig SetSigHeldAction(this BasicTriList tl, uint sigNum, uint heldMs, Action heldAction, Action releaseAction) + { + return tl.BooleanOutput[sigNum].SetSigHeldAction(heldMs, heldAction, null, releaseAction); + } + + /// + /// Sets an action to a held sig, an action for the release of hold, as well as a released-without-hold action + /// + /// + public static BoolOutputSig SetSigHeldAction(this BasicTriList tl, uint sigNum, uint heldMs, Action heldAction, + Action holdReleasedAction, Action releaseAction) + { + return tl.BooleanOutput[sigNum].SetSigHeldAction(heldMs, heldAction, holdReleasedAction, releaseAction); + } + + /// + /// + /// + /// + /// + /// The Sig + public static UShortOutputSig SetUShortSigAction(this UShortOutputSig sig, Action a) + { + sig.UserObject = a; + return sig; + } + /// + /// + /// + /// + /// + /// + /// + public static UShortOutputSig SetUShortSigAction(this BasicTriList tl, uint sigNum, Action a) + { + return tl.UShortOutput[sigNum].SetUShortSigAction(a); + } + + /// + /// + /// + /// + /// + /// + public static StringOutputSig SetStringSigAction(this StringOutputSig sig, Action a) + { + sig.UserObject = a; + return sig; + } + + /// + /// + /// + /// + /// + /// + /// + public static StringOutputSig SetStringSigAction(this BasicTriList tl, uint sigNum, Action a) + { + return tl.StringOutput[sigNum].SetStringSigAction(a); + } + + /// + /// + /// + /// + /// + public static Sig ClearSigAction(this Sig sig) + { + sig.UserObject = null; + return sig; + } + + public static BoolOutputSig ClearBoolSigAction(this BasicTriList tl, uint sigNum) + { + return ClearSigAction(tl.BooleanOutput[sigNum]) as BoolOutputSig; + } + + public static UShortOutputSig ClearUShortSigAction(this BasicTriList tl, uint sigNum) + { + return ClearSigAction(tl.UShortOutput[sigNum]) as UShortOutputSig; + } + + public static StringOutputSig ClearStringSigAction(this BasicTriList tl, uint sigNum) + { + return ClearSigAction(tl.StringOutput[sigNum]) as StringOutputSig; + } + + /// + /// Helper method to set the value of a bool Sig on TriList + /// + public static void SetBool(this BasicTriList tl, uint sigNum, bool value) + { + tl.BooleanInput[sigNum].BoolValue = value; + } + + /// + /// Sends an true-false pulse to the sig + /// + /// + /// + public static void PulseBool(this BasicTriList tl, uint sigNum) + { + tl.BooleanInput[sigNum].Pulse(); + } + + /// + /// Sends a timed pulse to the sig + /// + /// + /// + /// + public static void PulseBool(this BasicTriList tl, uint sigNum, int ms) + { + tl.BooleanInput[sigNum].Pulse(ms); + } + + /// + /// Helper method to set the value of a ushort Sig on TriList + /// + public static void SetUshort(this BasicTriList tl, uint sigNum, ushort value) + { + tl.UShortInput[sigNum].UShortValue = value; + } + + /// + /// Helper method to set the value of a string Sig on TriList + /// + public static void SetString(this BasicTriList tl, uint sigNum, string value) + { + tl.StringInput[sigNum].StringValue = value; + } + + /// + /// Returns bool value of trilist sig + /// + /// + /// + /// + public static bool GetBool(this BasicTriList tl, uint sigNum) + { + return tl.BooleanOutput[sigNum].BoolValue; + } + + /// + /// Returns ushort value of trilist sig + /// + /// + /// + /// + public static ushort GetUshort(this BasicTriList tl, uint sigNum) + { + return tl.UShortOutput[sigNum].UShortValue; + } + + /// + /// Returns string value of trilist sig. + /// + /// + /// + /// + public static string GetString(this BasicTriList tl, uint sigNum) + { + return tl.StringOutput[sigNum].StringValue; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/TriListBridges/HandlerBridge.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/TriListBridges/HandlerBridge.cs new file mode 100644 index 00000000..e1a69707 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/TriListBridges/HandlerBridge.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + + +namespace PepperDash.Essentials.Core +{ + public abstract class HandlerBridge + { + public bool IsAttached { get; protected set; } + + /// + /// Attaches the handler to the panel's user objects + /// + public abstract void AttachToTriListOutputs(bool sendUpdate); + + /// + /// Removes the handler from the panel's user objects + /// + public abstract void DetachFromTriListOutputs(); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/BlurayPageManager.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/BlurayPageManager.cs new file mode 100644 index 00000000..c272a65a --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/BlurayPageManager.cs @@ -0,0 +1,42 @@ +using Crestron.SimplSharpPro.DeviceSupport; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Core.PageManagers +{ + public class DiscPlayerMediumPageManager : MediumLeftSwitchablePageManager + { + IDiscPlayerControls Player; + + public DiscPlayerMediumPageManager(IDiscPlayerControls player, BasicTriListWithSmartObject trilist) + : base(player.DisplayUiType) + { + Player = player; + TriList = trilist; + } + + public override void Show() + { + uint offset = GetOffsetJoin(); + BackingPageJoin = offset + 1; + AllLeftSubpages = new uint[] { 7, 8 }; + + if (LeftSubpageJoin == 0) + LeftSubpageJoin = offset + 8; // default to transport + TriList.BooleanInput[BackingPageJoin].BoolValue = true; + TriList.BooleanInput[LeftSubpageJoin].BoolValue = true; + + // Attach buttons to interlock + foreach(var p in AllLeftSubpages) + { + var p2 = p; // scope + TriList.SetSigFalseAction(10000 + p2, () => InterlockLeftSubpage(p2)); + } + } + + public override void Hide() + { + TriList.BooleanInput[BackingPageJoin].BoolValue = false; + TriList.BooleanInput[LeftSubpageJoin].BoolValue = false; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/PageManager.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/PageManager.cs new file mode 100644 index 00000000..1f261f87 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/PageManager.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using Crestron.SimplSharpPro.DeviceSupport; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Core.PageManagers +{ + /// + /// The PageManager classes are used to bridge a device to subpage + /// visibility. + /// + public abstract class PageManager + { + protected List ActiveJoins = new List(); + + public abstract void Show(); + + public abstract void Hide(); + + /// + /// For device types 1-49, returns the offset join for subpage management 10100 - 14900 + /// + /// 1 through 49, as defined in some constants somewhere! + /// + public uint GetOffsetJoin(uint deviceType) + { + return 10000 + (deviceType * 100); + } + } + + /// + /// A simple class that hides and shows the default subpage for a given source type + /// + public class DefaultPageManager : PageManager + { + BasicTriList TriList; + uint BackingPageJoin; + + public DefaultPageManager(IUiDisplayInfo device, BasicTriList trilist) + { + TriList = trilist; + BackingPageJoin = GetOffsetJoin(device.DisplayUiType) + 1; + } + + public DefaultPageManager(uint join, BasicTriList trilist) + { + TriList = trilist; + BackingPageJoin = join; + } + + public override void Show() + { + TriList.BooleanInput[BackingPageJoin].BoolValue = true; + } + + public override void Hide() + { + TriList.BooleanInput[BackingPageJoin].BoolValue = false; + } + } + + /// + /// A page manager for a page with backing panel and a switchable side panel + /// + public abstract class MediumLeftSwitchablePageManager : PageManager + { + protected BasicTriListWithSmartObject TriList; + protected uint LeftSubpageJoin; + protected uint BackingPageJoin; + protected uint[] AllLeftSubpages; + protected uint DisplayUiType; + + protected MediumLeftSwitchablePageManager(uint displayUiType) + { + DisplayUiType = displayUiType; + } + + protected void InterlockLeftSubpage(uint join) + { + join = join + GetOffsetJoin(); + ClearLeftInterlock(); + TriList.BooleanInput[join].BoolValue = true; + LeftSubpageJoin = join; + } + + protected void ClearLeftInterlock() + { + foreach (var p in AllLeftSubpages) + TriList.BooleanInput[GetOffsetJoin() + p].BoolValue = false; + } + + protected uint GetOffsetJoin() + { + return GetOffsetJoin(DisplayUiType); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SetTopBoxThreePanelPageManager.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SetTopBoxThreePanelPageManager.cs new file mode 100644 index 00000000..f8838aed --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SetTopBoxThreePanelPageManager.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharpPro.DeviceSupport; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Presets; + +namespace PepperDash.Essentials.Core.PageManagers +{ + public class ThreePanelPlusOnePageManager : PageManager + { + protected BasicTriListWithSmartObject TriList; + + public uint Position5TabsId { get; set; } + + /// + /// Show the tabs on the third panel + /// + protected bool ShowPosition5Tabs; + + /// + /// Joins that are always visible when this manager is visible + /// + protected uint[] FixedVisibilityJoins; + + /// + /// + /// + protected uint CurrentVisiblePosition5Item; + + /// + /// + /// + /// + public ThreePanelPlusOnePageManager(BasicTriListWithSmartObject trilist) + { + TriList = trilist; + CurrentVisiblePosition5Item = 1; + } + + /// + /// The joins for the switchable panel in position 5 + /// + Dictionary Position5SubpageJoins = new Dictionary + { + { 1, 10053 }, + { 2, 10054 } + }; + + /// + /// + /// + public override void Show() + { + // Project the joins into corresponding sigs. + var fixedSigs = FixedVisibilityJoins.Select(u => TriList.BooleanInput[u]).ToList(); + foreach (var sig in fixedSigs) + sig.BoolValue = true; + + if (ShowPosition5Tabs) + { + // Show selected tab + TriList.BooleanInput[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = true; + // hook up tab object + var tabSo = TriList.SmartObjects[Position5TabsId]; + tabSo.BooleanOutput["Tab Button 1 Press"].UserObject = new Action(b => { if (!b) ShowTab(1); }); + tabSo.BooleanOutput["Tab Button 2 Press"].UserObject = new Action(b => { if (!b) ShowTab(2); }); + tabSo.SigChange -= tabSo_SigChange; + tabSo.SigChange += tabSo_SigChange; + } + } + + void tabSo_SigChange(Crestron.SimplSharpPro.GenericBase currentDevice, Crestron.SimplSharpPro.SmartObjectEventArgs args) + { + var uo = args.Sig.UserObject; + if(uo is Action) + (uo as Action)(args.Sig.BoolValue); + } + + public override void Hide() + { + var fixedSigs = FixedVisibilityJoins.Select(u => TriList.BooleanInput[u]).ToList(); + foreach (var sig in fixedSigs) + sig.BoolValue = false; + if (ShowPosition5Tabs) + { + TriList.BooleanInput[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = false; + + //var tabSo = TriList.SmartObjects[Position5TabsId]; + //tabSo.BooleanOutput["Tab Button 1 Press"].UserObject = null; + //tabSo.BooleanOutput["Tab Button 2 Press"].UserObject = null; + } + } + + void ShowTab(uint number) + { + // Ignore re-presses + if (CurrentVisiblePosition5Item == number) return; + // Swap subpage + var bi = TriList.BooleanInput; + if (CurrentVisiblePosition5Item > 0) + bi[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = false; + CurrentVisiblePosition5Item = number; + bi[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = true; + } + } + + + + public class SetTopBoxThreePanelPageManager : ThreePanelPlusOnePageManager + { + ISetTopBoxControls SetTopBox; + DevicePresetsView PresetsView; + + public uint DpadSmartObjectId { get; set; } + public uint NumberPadSmartObjectId { get; set; } + public uint PresetsSmartObjectId { get; set; } + + /// + /// A page manager for set top box that shows some combination of four different panels, + /// in three slots on the page. + /// + /// + /// + public SetTopBoxThreePanelPageManager(ISetTopBoxControls stb, BasicTriListWithSmartObject trilist) + : base(trilist) + { + SetTopBox = stb; + TriList = trilist; + + DpadSmartObjectId = 10011; + NumberPadSmartObjectId = 10014; + PresetsSmartObjectId = 10012; + Position5TabsId = 10081; + + bool dpad = stb.HasDpad; + bool preset = stb.HasPresets; + bool dvr = stb.HasDvr; + bool numbers = stb.HasNumeric; + + if (dpad && !preset && !dvr && !numbers) FixedVisibilityJoins = new uint[] { 10031, 10091 }; + else if (!dpad && preset && !dvr && !numbers) FixedVisibilityJoins = new uint[] { 10032, 10091 }; + else if (!dpad && !preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10033, 10091 }; + else if (!dpad && !preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10034, 10091 }; + + else if (dpad && preset && !dvr && !numbers) FixedVisibilityJoins = new uint[] { 10042, 10021, 10092 }; + else if (dpad && !preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10043, 10021, 10092 }; + else if (dpad && !preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10044, 10021, 10092 }; + else if (!dpad && preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10043, 10022, 10092 }; + else if (!dpad && preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10044, 10022, 10092 }; + else if (!dpad && !preset && dvr && numbers) FixedVisibilityJoins = new uint[] { 10044, 10023, 10092 }; + + else if (dpad && preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10053, 10032, 10011, 10093 }; + else if (dpad && preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10054, 10032, 10011, 10093 }; + else if (dpad && !preset && dvr && numbers) FixedVisibilityJoins = new uint[] { 10054, 10033, 10011, 10093 }; + else if (!dpad && preset && dvr && numbers) FixedVisibilityJoins = new uint[] { 10054, 10033, 10012, 10093 }; + + else if (dpad && preset && dvr && numbers) + { + FixedVisibilityJoins = new uint[] { 10081, 10032, 10011, 10093 }; // special case + ShowPosition5Tabs = true; + } + // Bad config case + else + { + Debug.Console(1, stb, "WARNING: Not configured to show any UI elements"); + FixedVisibilityJoins = new uint[] { 10091 }; + } + + // Build presets + if (stb.HasPresets && stb.PresetsModel != null) + { + PresetsView = new DevicePresetsView(trilist, stb.PresetsModel); + } + } + + public override void Show() + { + if(PresetsView != null) + PresetsView.Attach(); + base.Show(); + } + + public override void Hide() + { + if (PresetsView != null) + PresetsView.Detach(); + base.Hide(); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SetTopBoxTwoPanelPageManager.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SetTopBoxTwoPanelPageManager.cs new file mode 100644 index 00000000..cb1e1840 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SetTopBoxTwoPanelPageManager.cs @@ -0,0 +1,60 @@ +using Crestron.SimplSharpPro.DeviceSupport; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Presets; + +namespace PepperDash.Essentials.Core.PageManagers +{ + /// + /// A fixed-layout page manager that expects a DPad on the right, fixed portion of the page, and a two/three + /// tab switchable area on the left for presets, numeric and transport controls + /// + public class SetTopBoxMediumPageManager : MediumLeftSwitchablePageManager + { + ISetTopBoxControls SetTopBox; + DevicePresetsView PresetsView; + + public SetTopBoxMediumPageManager(ISetTopBoxControls stb, BasicTriListWithSmartObject trilist) + : base(stb.DisplayUiType) + { + SetTopBox = stb; + TriList = trilist; + if(stb.PresetsModel != null) + PresetsView = new DevicePresetsView(trilist, stb.PresetsModel); + } + + public override void Show() + { + if(PresetsView != null) + PresetsView.Attach(); + uint offset = GetOffsetJoin(); + if (SetTopBox.HasDvr) // Show backing page with DVR controls + { + BackingPageJoin = offset + 1; + AllLeftSubpages = new uint[] { 6, 7, 8 }; + } + else // Show the backing page with no DVR controls + { + BackingPageJoin = offset + 2; + AllLeftSubpages = new uint[] { 6, 7 }; + } + + if (LeftSubpageJoin == 0) + LeftSubpageJoin = offset + 6; // default to presets + TriList.BooleanInput[BackingPageJoin].BoolValue = true; + TriList.BooleanInput[LeftSubpageJoin].BoolValue = true; + + // Attach buttons to interlock + foreach(var p in AllLeftSubpages) + { + var p2 = p; // scope + TriList.SetSigFalseAction(10000 + p2, () => InterlockLeftSubpage(p2)); + } + } + + public override void Hide() + { + TriList.BooleanInput[BackingPageJoin].BoolValue = false; + TriList.BooleanInput[LeftSubpageJoin].BoolValue = false; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SinglePageManager.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SinglePageManager.cs new file mode 100644 index 00000000..dd7d605c --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/UI PageManagers/SinglePageManager.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Crestron.SimplSharpPro.DeviceSupport; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Core.PageManagers +{ + /// + /// A simple class that hides and shows the default subpage for a given source type + /// + public class SinglePageManager : PageManager + { + BasicTriList TriList; + uint BackingPageJoin; + + public SinglePageManager(uint pageJoin, BasicTriList trilist) + { + TriList = trilist; + BackingPageJoin = pageJoin; + } + + public override void Show() + { + TriList.BooleanInput[BackingPageJoin].BoolValue = true; + } + + public override void Hide() + { + TriList.BooleanInput[BackingPageJoin].BoolValue = false; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/VideoStatus/VideoStatusCues.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/VideoStatus/VideoStatusCues.cs new file mode 100644 index 00000000..5db78fb5 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/VideoStatus/VideoStatusCues.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +namespace PepperDash.Essentials.Core +{ + public static class VideoStatusCues + { + public static readonly Cue HasVideoStatusFeedback = Cue.BoolCue("HasVideoStatusFeedback", 1); + public static readonly Cue VideoSyncFeedback = Cue.BoolCue("VideoSyncFeedback", 2); + public static readonly Cue HdcpActiveFeedback = Cue.BoolCue("HdcpActiveFeedback", 3); + public static readonly Cue HdcpStateFeedback = Cue.StringCue("HdcpStateFeedback", 3); + public static readonly Cue VideoResolutionFeedback = Cue.StringCue("VideoResolutionFeedback", 2); + public static readonly Cue VideoStatusDeviceKey = Cue.StringCue("VideoStatusDeviceKey", 0); + public static readonly Cue VideoStatusDeviceName = Cue.StringCue("VideoStatusDeviceName", 4); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/VideoStatus/VideoStatusOutputs.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/VideoStatus/VideoStatusOutputs.cs new file mode 100644 index 00000000..62d0040a --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/VideoStatus/VideoStatusOutputs.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; + +namespace PepperDash.Essentials.Core +{ + /// + /// Use this class to pass in values to RoutingInputPorts. Unused properties will have default + /// funcs assigned to them. + /// + public class VideoStatusFuncsWrapper + { + public Func HasVideoStatusFunc { get; set; } + public Func HdcpActiveFeedbackFunc { get; set; } + public Func HdcpStateFeedbackFunc { get; set; } + public Func VideoResolutionFeedbackFunc { get; set; } + public Func VideoSyncFeedbackFunc { get; set; } + + public VideoStatusFuncsWrapper() + { + HasVideoStatusFunc = () => true; + HdcpActiveFeedbackFunc = () => false; + HdcpStateFeedbackFunc = () => ""; + VideoResolutionFeedbackFunc = () => "n/a"; + VideoSyncFeedbackFunc = () => false; + } + } + + /// + /// Wraps up the common video statuses exposed on a video input port + /// + public class VideoStatusOutputs + { + public BoolFeedback HasVideoStatusFeedback { get; private set; } + public BoolFeedback HdcpActiveFeedback { get; private set; } + public StringFeedback HdcpStateFeedback { get; private set; } + public StringFeedback VideoResolutionFeedback { get; private set; } + public BoolFeedback VideoSyncFeedback { get; private set; } + + /// + /// Gets the default, empty status group. + /// + public static VideoStatusOutputs NoStatus { get { return _Default; } } + static VideoStatusOutputs _Default = new VideoStatusOutputs(new VideoStatusFuncsWrapper + { + HasVideoStatusFunc = () => false + }); + + public VideoStatusOutputs(VideoStatusFuncsWrapper funcs) + { + HasVideoStatusFeedback = new BoolFeedback("HasVideoStatusFeedback", funcs.HasVideoStatusFunc); + HdcpActiveFeedback = new BoolFeedback("HdcpActiveFeedback", funcs.HdcpActiveFeedbackFunc); + HdcpStateFeedback = new StringFeedback("HdcpStateFeedback", funcs.HdcpStateFeedbackFunc); + VideoResolutionFeedback = new StringFeedback("VideoResolutionFeedback", + funcs.VideoResolutionFeedbackFunc); + VideoSyncFeedback = new BoolFeedback("VideoSyncFeedback", funcs.VideoSyncFeedbackFunc); + } + + public void FireAll() + { + HasVideoStatusFeedback.FireUpdate(); + HdcpActiveFeedback.FireUpdate(); + HdcpActiveFeedback.FireUpdate(); + VideoResolutionFeedback.FireUpdate(); + VideoSyncFeedback.FireUpdate(); + } + + public List ToList() + { + return new List + { + HasVideoStatusFeedback, + HdcpActiveFeedback, + HdcpStateFeedback, + VideoResolutionFeedback, + VideoSyncFeedback + }; + } + } + + ///// + ///// Wraps up the common video statuses exposed on a video input port + ///// + //public class BasicVideoStatus : IBasicVideoStatus + //{ + // public event VideoStatusChangeHandler VideoStatusChange; + + // public bool HasVideoStatus { get; private set; } + + // public bool HdcpActive + // { + // get { return HdcpActiveFunc != null ? HdcpActiveFunc() : false; } + // } + + // public string HdcpState + // { + // get { return HdcpStateFunc != null? HdcpStateFunc() : ""; } + // } + + // public string VideoResolution + // { + // get { return VideoResolutionFunc != null ? VideoResolutionFunc() : ""; } + // } + + // public bool VideoSync + // { + // get { return VideoSyncFunc != null ? VideoSyncFunc() : false; } + // } + + // Func HdcpActiveFunc; + // Func HdcpStateFunc; + // Func VideoResolutionFunc; + // Func VideoSyncFunc; + + // public BasicVideoStatus(bool hasVideoStatus, Func hdcpActiveFunc, + // Func hdcpStateFunc, Func videoResolutionFunc, Func videoSyncFunc) + // { + // HasVideoStatus = hasVideoStatus; + // HdcpActiveFunc = hdcpActiveFunc; + // HdcpStateFunc = hdcpStateFunc; + // VideoResolutionFunc = videoResolutionFunc; + // VideoSyncFunc = videoSyncFunc; + // } + //} + + //public enum eVideoStatusChangeType + //{ + // HdcpActive, + // HdcpState, + // VideoResolution, + // VideoSync + //} + + //public interface IBasicVideoStatus + //{ + // event VideoStatusChangeHandler VideoStatusChange; + // bool HdcpActive { get; } + // string HdcpState { get; } + // string VideoResolution { get; } + // bool VideoSync { get; } + //} + + //public delegate void VideoStatusChangeHandler(IBasicVideoStatus device, eVideoStatusChangeType type); +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/app.config b/essentials-framework/Essentials Core/PepperDashEssentialsBase/app.config new file mode 100644 index 00000000..1e79e7b5 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/essentials-framework/Essentials DM/Essentials_DM.sln b/essentials-framework/Essentials DM/Essentials_DM.sln new file mode 100644 index 00000000..fa874f80 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials_DM", "Essentials_DM\Essentials_DM.csproj", "{9199CE8A-0C9F-4952-8672-3EED798B284F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9199CE8A-0C9F-4952-8672-3EED798B284F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9199CE8A-0C9F-4952-8672-3EED798B284F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9199CE8A-0C9F-4952-8672-3EED798B284F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9199CE8A-0C9F-4952-8672-3EED798B284F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/essentials-framework/Essentials DM/Essentials_DM/AirMedia/AirMediaController.cs b/essentials-framework/Essentials DM/Essentials_DM/AirMedia/AirMediaController.cs new file mode 100644 index 00000000..b4ee9784 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/AirMedia/AirMediaController.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.DeviceSupport.Support; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.AirMedia; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.DM.AirMedia +{ + public class AirMediaController : CrestronGenericBaseDevice, IRoutingInputsOutputs, IIROutputPorts, IComPorts + { + public AmX00 AirMedia { get; private set; } + + public DeviceConfig DeviceConfig { get; private set; } + + AirMediaPropertiesConfig PropertiesConfig; + + public RoutingPortCollection InputPorts { get; private set; } + + public RoutingPortCollection OutputPorts { get; private set; } + + public BoolFeedback IsInSessionFeedback { get; private set; } + public IntFeedback ErrorFeedback { get; private set; } + public IntFeedback NumberOfUsersConnectedFeedback { get; set; } + public IntFeedback LoginCodeFeedback { get; set; } + public StringFeedback ConnectionAddressFeedback { get; set; } + public StringFeedback HostnameFeedback { get; set; } + public IntFeedback VideoOutFeedback { get; private set; } + public BoolFeedback HdmiVideoSyncDetectedFeedback { get; private set; } + public StringFeedback SerialNumberFeedback { get; private set; } + public BoolFeedback AutomaticInputRoutingEnabledFeedback { get; private set; } + + public AirMediaController(string key, string name, AmX00 device, DeviceConfig dc, AirMediaPropertiesConfig props) + :base(key, name, device) + { + AirMedia = device; + + DeviceConfig = dc; + + PropertiesConfig = props; + + InputPorts.Add(new RoutingInputPort(DmPortName.Osd, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.None, new Action(SelectPinPointUxLandingPage), this)); + + InputPorts.Add(new RoutingInputPort(DmPortName.AirMediaIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Streaming, new Action(SelectAirMedia), this)); + + InputPorts.Add(new RoutingInputPort(DmPortName.HdmiIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(SelectHdmiIn), this)); + + InputPorts.Add(new RoutingInputPort(DmPortName.AirBoardIn, eRoutingSignalType.Video, + eRoutingPortConnectionType.None, new Action(SelectAirboardIn), this)); + + if (AirMedia is Am300) + { + InputPorts.Add(new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, new Action(SelectDmIn), this)); + } + + AirMedia.AirMedia.AirMediaChange += new Crestron.SimplSharpPro.DeviceSupport.GenericEventHandler(AirMedia_AirMediaChange); + + IsInSessionFeedback = new BoolFeedback( new Func(() => AirMedia.AirMedia.StatusFeedback.UShortValue == 0 )); + ErrorFeedback = new IntFeedback(new Func(() => AirMedia.AirMedia.ErrorFeedback.UShortValue)); + NumberOfUsersConnectedFeedback = new IntFeedback(new Func(() => AirMedia.AirMedia.NumberOfUsersConnectedFeedback.UShortValue)); + LoginCodeFeedback = new IntFeedback(new Func(() => AirMedia.AirMedia.LoginCodeFeedback.UShortValue)); + ConnectionAddressFeedback = new StringFeedback(new Func(() => AirMedia.AirMedia.ConnectionAddressFeedback.StringValue)); + HostnameFeedback = new StringFeedback(new Func(() => AirMedia.AirMedia.HostNameFeedback.StringValue)); + + // TODO: Figure out if we can actually get the TSID/Serial + SerialNumberFeedback = new StringFeedback(new Func(() => "unknown")); + + AirMedia.DisplayControl.DisplayControlChange += new Crestron.SimplSharpPro.DeviceSupport.GenericEventHandler(DisplayControl_DisplayControlChange); + + VideoOutFeedback = new IntFeedback(new Func(() => Convert.ToInt16(AirMedia.DisplayControl.VideoOutFeedback))); + AutomaticInputRoutingEnabledFeedback = new BoolFeedback(new Func(() => AirMedia.DisplayControl.EnableAutomaticRoutingFeedback.BoolValue)); + + AirMedia.HdmiIn.StreamChange += new Crestron.SimplSharpPro.DeviceSupport.StreamEventHandler(HdmiIn_StreamChange); + + HdmiVideoSyncDetectedFeedback = new BoolFeedback(new Func(() => AirMedia.HdmiIn.SyncDetectedFeedback.BoolValue)); + } + + public override bool CustomActivate() + { + if (PropertiesConfig.AutoSwitchingEnabled) + AirMedia.DisplayControl.EnableAutomaticRouting(); + else + AirMedia.DisplayControl.DisableAutomaticRouting(); + + return base.CustomActivate(); + } + + void AirMedia_AirMediaChange(object sender, Crestron.SimplSharpPro.DeviceSupport.GenericEventArgs args) + { + if (args.EventId == AirMediaInputSlot.AirMediaStatusFeedbackEventId) + IsInSessionFeedback.FireUpdate(); + else if (args.EventId == AirMediaInputSlot.AirMediaErrorFeedbackEventId) + ErrorFeedback.FireUpdate(); + else if (args.EventId == AirMediaInputSlot.AirMediaNumberOfUserConnectedEventId) + NumberOfUsersConnectedFeedback.FireUpdate(); + else if (args.EventId == AirMediaInputSlot.AirMediaLoginCodeEventId) + LoginCodeFeedback.FireUpdate(); + else if (args.EventId == AirMediaInputSlot.AirMediaConnectionAddressFeedbackEventId) + ConnectionAddressFeedback.FireUpdate(); + else if (args.EventId == AirMediaInputSlot.AirMediaHostNameFeedbackEventId) + HostnameFeedback.FireUpdate(); + } + + void DisplayControl_DisplayControlChange(object sender, Crestron.SimplSharpPro.DeviceSupport.GenericEventArgs args) + { + if (args.EventId == AmX00.VideoOutFeedbackEventId) + VideoOutFeedback.FireUpdate(); + else if (args.EventId == AmX00.EnableAutomaticRoutingFeedbackEventId) + AutomaticInputRoutingEnabledFeedback.FireUpdate(); + } + + void HdmiIn_StreamChange(Crestron.SimplSharpPro.DeviceSupport.Stream stream, Crestron.SimplSharpPro.DeviceSupport.StreamEventArgs args) + { + if (args.EventId == DMInputEventIds.SourceSyncEventId) + HdmiVideoSyncDetectedFeedback.FireUpdate(); + } + + /// + /// Sets the VideoOut source ( 0 = PinpointUX, 1 = AirMedia, 2 = HDMI, 3 = DM, 4 = Airboard ) + /// + /// source number + public void SelectVideoOut(uint source) + { + AirMedia.DisplayControl.VideoOut = (AmX00DisplayControl.eAirMediaX00VideoSource)source; + } + + /// + /// Selects the PinPointUXLandingPage input + /// + public void SelectPinPointUxLandingPage() + { + AirMedia.DisplayControl.VideoOut = AmX00DisplayControl.eAirMediaX00VideoSource.PinPointUxLandingPage; + } + + /// + /// Selects the AirMedia input + /// + public void SelectAirMedia() + { + AirMedia.DisplayControl.VideoOut = AmX00DisplayControl.eAirMediaX00VideoSource.AirMedia; + } + + /// + /// Selects the DM input + /// + public void SelectDmIn() + { + AirMedia.DisplayControl.VideoOut = AmX00DisplayControl.eAirMediaX00VideoSource.HDMI; + } + + /// + /// Selects the HDMI INput + /// + public void SelectHdmiIn() + { + AirMedia.DisplayControl.VideoOut = AmX00DisplayControl.eAirMediaX00VideoSource.DM; + } + + public void SelectAirboardIn() + { + AirMedia.DisplayControl.VideoOut = AmX00DisplayControl.eAirMediaX00VideoSource.AirBoard; + } + + /// + /// Reboots the device + /// + public void RebootDevice() + { + AirMedia.AirMedia.DeviceReboot(); + } + + #region IIROutputPorts Members + + public CrestronCollection IROutputPorts + { + get { return AirMedia.IROutputPorts; } + } + + public int NumberOfIROutputPorts + { + get { return AirMedia.NumberOfIROutputPorts; } + } + + + + #endregion + + + + #region IComPorts Members + + public CrestronCollection ComPorts + { + get { return AirMedia.ComPorts; } + } + + public int NumberOfComPorts + { + get { return AirMedia.NumberOfComPorts; } + } + + #endregion + + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/AirMedia/AirMediaPropertiesConfig.cs b/essentials-framework/Essentials DM/Essentials_DM/AirMedia/AirMediaPropertiesConfig.cs new file mode 100644 index 00000000..b89bd989 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/AirMedia/AirMediaPropertiesConfig.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + +using Newtonsoft.Json; + +namespace PepperDash.Essentials.DM.AirMedia +{ + public class AirMediaPropertiesConfig + { + [JsonProperty("control")] + public ControlPropertiesConfig Control { get; set; } + + [JsonProperty("autoSwitching")] + public bool AutoSwitchingEnabled { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmInputCardBase.cs b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmInputCardBase.cs new file mode 100644 index 00000000..a0c9536c --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmInputCardBase.cs @@ -0,0 +1,62 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +//using Crestron.SimplSharpPro.DM; +//using Crestron.SimplSharpPro.DM.Cards; + +//using PepperDash.Essentials.Core; +//using PepperDash.Essentials.DM; + +//namespace PepperDash.Essentials.DM.Cards +//{ +// public class DmInputCardControllerBase : IRoutingInputsOutputs +// { +// public string Key { get; private set; } +// public uint Slot { get; private set; } +// public abstract eDmInputCardType Type { get; } + +// //public RoutingOutputPort BackplaneVideoOut { get; private set; } +// //public RoutingOutputPort BackplaneAudioOut { get; private set; } + +// public RoutingPortCollection InputPorts { get; private set; } + +// public RoutingPortCollection OutputPorts { get; private set; } + +// public DmInputCardControllerBase(string key, uint slot) +// { +// Key = key; +// Slot = slot; +// //BackplaneAudioOut = new RoutingOutputPort("backplaneAudioOut", eRoutingSignalType.Audio, +// // eRoutingPortConnectionType.BackplaneOnly, slot, this); +// //BackplaneVideoOut = new RoutingOutputPort("backplaneVideoOut", eRoutingSignalType.Video, +// // eRoutingPortConnectionType.BackplaneOnly, slot, this); +// //InputPorts = new RoutingPortCollection(); +// //OutputPorts = new RoutingPortCollection { BackplaneAudioOut, BackplaneVideoOut }; +// } + +// ///// +// ///// Gets a physical port by name. Returns null if doesn't exist +// ///// +// //public RoutingInputPort GetInputPort(string key) +// //{ +// // return InputPorts.FirstOrDefault(p => p.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); +// //} + +// ///// +// ///// Gets a physical port by name. Returns null if doesn't exist +// ///// +// //public RoutingOutputPort GetOutputPort(string key) +// //{ +// // return OutputPorts.FirstOrDefault(p => p.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); +// //} +// } + +// public enum eDmInputCardType +// { +// None, DmcHd, DmcHdDsp, Dmc4kHd, Dmc4kHdDsp, Dmc4kC, Dmc4kCDsp +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmOutputCardBase.cs b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmOutputCardBase.cs new file mode 100644 index 00000000..c554539a --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmOutputCardBase.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Cards; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM; + +namespace PepperDash.Essentials.DM.Cards +{ + /// + /// + /// + public abstract class DmSingleOutputCardControllerBase// : IRoutingInputsOutputs + { + public string Key { get; private set; } + public uint Slot { get; private set; } + public abstract eDmOutputCardType Type { get; } + + //public RoutingInputPort BackplaneAudioIn1 { get; private set; } + //public RoutingInputPort BackplaneVideoIn1 { get; private set; } + //public RoutingInputPort BackplaneAudioIn2 { get; private set; } + //public RoutingInputPort BackplaneVideoIn2 { get; private set; } + + //public RoutingPortCollection InputPorts { get; private set; } + //public RoutingPortCollection OutputPorts { get; private set; } + + public DmSingleOutputCardControllerBase(string key, uint cardSlot) + { + Key = key; + Slot = cardSlot; + //BackplaneAudioIn1 = new RoutingInputPort("backplaneAudioIn1", eRoutingSignalType.Audio, + // eRoutingPortConnectionType.BackplaneOnly, cardSlot, this); + //BackplaneVideoIn1 = new RoutingInputPort("backplaneVideoIn1", eRoutingSignalType.Video, + // eRoutingPortConnectionType.BackplaneOnly, cardSlot, this); + //BackplaneAudioIn2 = new RoutingInputPort("backplaneAudioIn2", eRoutingSignalType.Audio, + // eRoutingPortConnectionType.BackplaneOnly, cardSlot + 1, this); + //BackplaneVideoIn2 = new RoutingInputPort("backplaneVideoIn2", eRoutingSignalType.Video, + // eRoutingPortConnectionType.BackplaneOnly, cardSlot + 1, this); + //InputPorts = new RoutingPortCollection + //{ + // BackplaneAudioIn1, + // BackplaneAudioIn2, + // BackplaneVideoIn1, + // BackplaneVideoIn2 + //}; + //OutputPorts = new RoutingPortCollection(); + } + + ///// + ///// Gets a physical port by name. Returns null if doesn't exist + ///// + //public RoutingInputPort GetInputPort(string key) + //{ + // return InputPorts.FirstOrDefault(p => p.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + //} + + ///// + ///// Gets a physical port by name. Returns null if doesn't exist + ///// + //public RoutingOutputPort GetOutputPort(string key) + //{ + // return OutputPorts.FirstOrDefault(p => p.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + //} + } + + /// + /// + /// + public enum eDmOutputCardType + { + None, Dmc4kCoHd, Dmc4kHdo, DmcCoHd, DmcSoHd + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/Dmc4kCoHdSingleOutputCard.cs b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/Dmc4kCoHdSingleOutputCard.cs new file mode 100644 index 00000000..3e3fd309 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/Dmc4kCoHdSingleOutputCard.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Cards; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM; + +namespace PepperDash.Essentials.DM.Cards +{ + public class Dmc4kCoHdSingleOutputCard : DmSingleOutputCardControllerBase + { + public override eDmOutputCardType Type + { + get { return eDmOutputCardType.Dmc4kCoHd; } + } + public Dmc4kCoHdSingle Card { get; private set; } + + //public RoutingOutputPort DmOut1 { get; private set; } + //public RoutingOutputPort DmOut2 { get; private set; } + //public RoutingOutputPort HdmiOut1 { get; private set; } + + public Dmc4kCoHdSingleOutputCard(string key, Dmc4kCoHdSingle card, uint slot) + : base(key, slot) + { + Card = card; + //DmOut1 = new RoutingOutputPort(DmPortName.DmOut1, eRoutingSignalType.AudioVideo, + // eRoutingPortConnectionType.DmCat, null, this); + //DmOut2 = new RoutingOutputPort(DmPortName.DmOut2, eRoutingSignalType.AudioVideo, + // eRoutingPortConnectionType.DmCat, null, this); + //HdmiOut1 = new RoutingOutputPort(DmPortName.HdmiOut1, eRoutingSignalType.AudioVideo, + // eRoutingPortConnectionType.Hdmi, null, this); + + //OutputPorts.AddRange(new[] { DmOut1, DmOut2, HdmiOut1 }); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/Dmc4kHdoSingleOutputCard.cs b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/Dmc4kHdoSingleOutputCard.cs new file mode 100644 index 00000000..b15c4006 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/Dmc4kHdoSingleOutputCard.cs @@ -0,0 +1,46 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +//using Crestron.SimplSharpPro.DM; +//using Crestron.SimplSharpPro.DM.Cards; + +//using PepperDash.Core; +//using PepperDash.Essentials.Core; +//using PepperDash.Essentials.DM; + +//namespace PepperDash.Essentials.DM.Cards +//{ +// public class Dmc4kHdoSingleOutputCard : DmSingleOutputCardControllerBase +// { +// public override eDmOutputCardType Type +// { +// get { return eDmOutputCardType.Dmc4kHdo; } +// } +// public Dmc4kHdoSingle Card { get; private set; } + +// //public RoutingOutputPort AudioOut1 { get; private set; } +// //public RoutingOutputPort AudioOut2 { get; private set; } +// //public RoutingOutputPort HdmiOut1 { get; private set; } +// //public RoutingOutputPort HdmiOut2 { get; private set; } + +// public Dmc4kHdoSingleOutputCard(string key, Dmc4kHdoSingle card, uint slot) +// : base(key, slot) +// { +// Card = card; +// //AudioOut1 = new RoutingOutputPort(DmPortName.BalancedAudioOut1, eRoutingSignalType.Audio, +// // eRoutingPortConnectionType.LineAudio, null, this); +// //AudioOut2 = new RoutingOutputPort(DmPortName.BalancedAudioOut2, eRoutingSignalType.Audio, +// // eRoutingPortConnectionType.LineAudio, null, this); +// //HdmiOut1 = new RoutingOutputPort(DmPortName.HdmiOut1, eRoutingSignalType.AudioVideo, +// // eRoutingPortConnectionType.Hdmi, null, this); +// //HdmiOut2 = new RoutingOutputPort(DmPortName.HdmiOut2, eRoutingSignalType.AudioVideo, +// // eRoutingPortConnectionType.Hdmi, null, this); + +// //OutputPorts.AddRange(new[] { AudioOut1, AudioOut2, HdmiOut1, HdmiOut2 }); +// } +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmcC4kInputCard.cs b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmcC4kInputCard.cs new file mode 100644 index 00000000..40f6044c --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmcC4kInputCard.cs @@ -0,0 +1,76 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +//using Crestron.SimplSharpPro.DM; +//using Crestron.SimplSharpPro.DM.Cards; + +//using PepperDash.Essentials.Core; +//using PepperDash.Essentials.DM; + +//namespace PepperDash.Essentials.DM.Cards +//{ +// public class Dmc4kCController : DmInputCardControllerBase +// { +// public override eDmInputCardType Type +// { +// get { return eDmInputCardType.Dmc4kC; } +// } +// public Dmc4kC Card { get; private set; } + +// //public RoutingInputPortWithVideoStatuses DmIn { get; private set; } +// //public RoutingOutputPort HdmiLoopOut { get; private set; } +// //public RoutingOutputPort AudioLoopOut { get; private set; } + +// public Dmc4kCController(string key, Dmc4kC card, uint slot) +// : base(key, slot) +// { +// Card = card; +// //DmIn = new RoutingInputPortWithVideoStatuses(DmPortName.DmIn, eRoutingSignalType.AudioVideo, +// // eRoutingPortConnectionType.DmCat, null, this, +// // VideoStatusHelper.GetDmInputStatusFuncs(Card.DmInput)); + +// //HdmiLoopOut = new RoutingOutputPort(DmPortName.HdmiLoopOut, eRoutingSignalType.AudioVideo, +// // eRoutingPortConnectionType.Hdmi, null, this); +// //AudioLoopOut = new RoutingOutputPort(DmPortName.AudioLoopOut, eRoutingSignalType.Audio, +// // eRoutingPortConnectionType.Hdmi, null, this); + +// //InputPorts.Add(DmIn); +// //OutputPorts.AddRange(new[] { HdmiLoopOut, AudioLoopOut }); +// } +// } + +// public class Dmc4kCDspController : DmInputCardControllerBase +// { +// public override eDmInputCardType Type +// { +// get { return eDmInputCardType.Dmc4kCDsp; } +// } +// public Dmc4kCDsp Card { get; private set; } + +// //public RoutingInputPortWithVideoStatuses DmIn { get; private set; } +// //public RoutingOutputPort HdmiLoopOut { get; private set; } +// //public RoutingOutputPort AudioLoopOut { get; private set; } + +// public Dmc4kCDspController(string key, Dmc4kCDsp card, uint slot) +// : base(key, slot) +// { +// Card = card; +// //DmIn = new RoutingInputPortWithVideoStatuses(DmPortName.DmIn, eRoutingSignalType.AudioVideo, +// // eRoutingPortConnectionType.DmCat, null, this, +// // VideoStatusHelper.GetDmInputStatusFuncs(Card.DmInput)); + +// //HdmiLoopOut = new RoutingOutputPort(DmPortName.HdmiLoopOut, eRoutingSignalType.AudioVideo, +// // eRoutingPortConnectionType.Hdmi, null, this); +// //AudioLoopOut = new RoutingOutputPort(DmPortName.AudioLoopOut, eRoutingSignalType.Audio, +// // eRoutingPortConnectionType.Hdmi, null, this); + +// //InputPorts.Add(DmIn); +// //OutputPorts.AddRange(new[] { HdmiLoopOut, AudioLoopOut }); +// } +// } + +//} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmcHD4kInputCard.cs b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmcHD4kInputCard.cs new file mode 100644 index 00000000..e317085c --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Cards REMOVE/DmcHD4kInputCard.cs @@ -0,0 +1,82 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +//using Crestron.SimplSharpPro.DM; +//using Crestron.SimplSharpPro.DM.Cards; + +//using PepperDash.Essentials.Core; +//using PepperDash.Essentials.DM; + +//namespace PepperDash.Essentials.DM.Cards +//{ +// /// +// /// +// /// +// public class Dmc4kHdController : DmInputCardControllerBase +// { +// public Dmc4kHd Card { get; private set; } +// public override eDmInputCardType Type +// { +// get { return eDmInputCardType.Dmc4kHd; } +// } + +// public RoutingInputPortWithVideoStatuses HdmiIn { get; private set; } +// public RoutingOutputPort HdmiLoopOut { get; private set; } +// public RoutingOutputPort AudioLoopOut { get; private set; } + +// public Dmc4kHdController(string key, Dmc4kHd card, uint slot) +// : base(key, slot) +// { +// Card = card; +// HdmiIn = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn, eRoutingSignalType.AudioVideo, +// eRoutingPortConnectionType.Hdmi, null, this, +// VideoStatusHelper.GetHdmiInputStatusFuncs(Card.HdmiInput)); + +// HdmiLoopOut = new RoutingOutputPort(DmPortName.HdmiLoopOut, eRoutingSignalType.AudioVideo, +// eRoutingPortConnectionType.Hdmi, null, this); +// AudioLoopOut = new RoutingOutputPort(DmPortName.AudioLoopOut, eRoutingSignalType.Audio, +// eRoutingPortConnectionType.Hdmi, null, this); + +// InputPorts.Add(HdmiIn); +// OutputPorts.AddRange(new[] { HdmiLoopOut, AudioLoopOut }); +// } +// } + +// /// +// /// +// /// +// public class Dmc4kHdDspController : DmInputCardControllerBase +// { +// public Dmc4kHdDsp Card { get; private set; } +// public override eDmInputCardType Type +// { +// get { return eDmInputCardType.Dmc4kHdDsp; } +// } + +// //public RoutingInputPortWithVideoStatuses HdmiIn { get; private set; } +// //public RoutingOutputPort HdmiLoopOut { get; private set; } +// //public RoutingOutputPort AudioLoopOut { get; private set; } + +// public Dmc4kHdDspController(string key, Dmc4kHdDsp card, uint slot) +// : base(key, slot) +// { +// Card = card; +// //HdmiIn = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn, eRoutingSignalType.AudioVideo, +// // eRoutingPortConnectionType.Hdmi, null, this, +// // VideoStatusHelper.GetHdmiInputStatusFuncs(Card.HdmiInput)); + +// //HdmiLoopOut = new RoutingOutputPort(DmPortName.HdmiLoopOut, eRoutingSignalType.AudioVideo, +// // eRoutingPortConnectionType.Hdmi, null, this); +// //AudioLoopOut = new RoutingOutputPort(DmPortName.AudioLoopOut, eRoutingSignalType.Audio, +// // eRoutingPortConnectionType.Hdmi, null, this); + +// //InputPorts.Add(HdmiIn); +// //OutputPorts.AddRange(new[] { HdmiLoopOut, AudioLoopOut }); +// } +// } +//} + diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/._DmChassisController.cs b/essentials-framework/Essentials DM/Essentials_DM/Chassis/._DmChassisController.cs new file mode 100644 index 0000000000000000000000000000000000000000..a5ed710ce40218e06fa16ad90c1755b28dd50a73 GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103v3S56% z0-}Rq0Z_RBnifVNA1W@DoS&aZoj0; z2BJs7Xb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeD;0ggyXA^|MKrSRBvsj@h zwK%`DC^=OjEx#yRAv3QeHLoNyKQA#Sr&1v&HLXM;DJL;68`u|y>Kf7%s{i3$kztVg G{~rK(GAqdd literal 0 HcmV?d00001 diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput.cs b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput.cs new file mode 100644 index 00000000..4b4a927e --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + public class DmCardAudioOutputController : IBasicVolumeWithFeedback + { + public Audio.Output Output { get; private set; } + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + ushort PreMuteVolumeLevel; + bool IsMuted; + + public DmCardAudioOutputController(Audio.Output output) + { + Output = output; + VolumeLevelFeedback = new IntFeedback(() => Output.VolumeFeedback.UShortValue); + MuteFeedback = new BoolFeedback(() => IsMuted); + } + + #region IBasicVolumeWithFeedback Members + + /// + /// + /// + public void MuteOff() + { + SetVolume(PreMuteVolumeLevel); + IsMuted = false; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void MuteOn() + { + PreMuteVolumeLevel = Output.VolumeFeedback.UShortValue; + SetVolume(0); + IsMuted = true; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void SetVolume(ushort level) + { + Debug.Console(2, "Set volume out {0}", level); + Output.Volume.UShortValue = level; + } + + /// + /// + /// + internal void VolumeEventFromChassis() + { + VolumeLevelFeedback.FireUpdate(); + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// + /// + public void MuteToggle() + { + if (IsMuted) + MuteOff(); + else + MuteOn(); + } + + /// + /// + /// + public void VolumeDown(bool pressRelease) + { + if (pressRelease) + { + var remainingRatio = Output.Volume.UShortValue / 65535; + Output.Volume.CreateRamp(0, (uint)(400 * remainingRatio)); + } + else + Output.Volume.StopRamp(); + } + + /// + /// + /// + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + { + var remainingRatio = (65535 - Output.Volume.UShortValue) / 65535; + Output.Volume.CreateRamp(65535, 400); + } + else + Output.Volume.StopRamp(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput.cs.orig b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput.cs.orig new file mode 100644 index 00000000..acaf7916 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput.cs.orig @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + public class DmCardAudioOutputController : IBasicVolumeWithFeedback + { + public Audio.Output Output { get; private set; } + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + ushort PreMuteVolumeLevel; + bool IsMuted; + + public DmCardAudioOutputController(Audio.Output output) + { + Output = output; + VolumeLevelFeedback = new IntFeedback(() => Output.VolumeFeedback.UShortValue); + MuteFeedback = new BoolFeedback(() => IsMuted); + } + + #region IBasicVolumeWithFeedback Members + + /// + /// + /// + public void MuteOff() + { + SetVolume(PreMuteVolumeLevel); + IsMuted = false; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void MuteOn() + { + PreMuteVolumeLevel = Output.VolumeFeedback.UShortValue; + SetVolume(0); + IsMuted = true; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void SetVolume(ushort level) + { + Debug.Console(2, "Set volume out {0}", level); + Output.Volume.UShortValue = level; + } + + /// + /// + /// + internal void VolumeEventFromChassis() + { + VolumeLevelFeedback.FireUpdate(); + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// + /// + public void MuteToggle() + { + if (IsMuted) + MuteOff(); + else + MuteOn(); + } + + /// + /// + /// + public void VolumeDown(bool pressRelease) + { + if (pressRelease) +<<<<<<< HEAD + { + var remainingRatio = Output.Volume.UShortValue / 65535; + Output.Volume.CreateRamp(0, (uint)(400 * remainingRatio)); + } +======= + Output.Volume.CreateRamp(0, 400); +>>>>>>> origin/feature/fusion-nyu + else + Output.Volume.StopRamp(); + } + + /// + /// + /// + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + { + var remainingRatio = (65535 - Output.Volume.UShortValue) / 65535; + Output.Volume.CreateRamp(65535, 400); + } + else + Output.Volume.StopRamp(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_BACKUP_14408.cs b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_BACKUP_14408.cs new file mode 100644 index 00000000..acaf7916 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_BACKUP_14408.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + public class DmCardAudioOutputController : IBasicVolumeWithFeedback + { + public Audio.Output Output { get; private set; } + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + ushort PreMuteVolumeLevel; + bool IsMuted; + + public DmCardAudioOutputController(Audio.Output output) + { + Output = output; + VolumeLevelFeedback = new IntFeedback(() => Output.VolumeFeedback.UShortValue); + MuteFeedback = new BoolFeedback(() => IsMuted); + } + + #region IBasicVolumeWithFeedback Members + + /// + /// + /// + public void MuteOff() + { + SetVolume(PreMuteVolumeLevel); + IsMuted = false; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void MuteOn() + { + PreMuteVolumeLevel = Output.VolumeFeedback.UShortValue; + SetVolume(0); + IsMuted = true; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void SetVolume(ushort level) + { + Debug.Console(2, "Set volume out {0}", level); + Output.Volume.UShortValue = level; + } + + /// + /// + /// + internal void VolumeEventFromChassis() + { + VolumeLevelFeedback.FireUpdate(); + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// + /// + public void MuteToggle() + { + if (IsMuted) + MuteOff(); + else + MuteOn(); + } + + /// + /// + /// + public void VolumeDown(bool pressRelease) + { + if (pressRelease) +<<<<<<< HEAD + { + var remainingRatio = Output.Volume.UShortValue / 65535; + Output.Volume.CreateRamp(0, (uint)(400 * remainingRatio)); + } +======= + Output.Volume.CreateRamp(0, 400); +>>>>>>> origin/feature/fusion-nyu + else + Output.Volume.StopRamp(); + } + + /// + /// + /// + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + { + var remainingRatio = (65535 - Output.Volume.UShortValue) / 65535; + Output.Volume.CreateRamp(65535, 400); + } + else + Output.Volume.StopRamp(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_BASE_14408.cs b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_BASE_14408.cs new file mode 100644 index 00000000..3a069c39 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_BASE_14408.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + public class DmCardAudioOutputController : IBasicVolumeWithFeedback + { + public Audio.Output Output { get; private set; } + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + ushort PreMuteVolumeLevel; + bool IsMuted; + + public DmCardAudioOutputController(Audio.Output output) + { + Output = output; + VolumeLevelFeedback = new IntFeedback(() => Output.VolumeFeedback.UShortValue); + MuteFeedback = new BoolFeedback(() => IsMuted); + } + + #region IBasicVolumeWithFeedback Members + + /// + /// + /// + public void MuteOff() + { + SetVolume(PreMuteVolumeLevel); + IsMuted = false; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void MuteOn() + { + PreMuteVolumeLevel = Output.VolumeFeedback.UShortValue; + SetVolume(0); + IsMuted = true; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void SetVolume(ushort level) + { + Debug.Console(2, "Set volume out {0}", level); + Output.Volume.UShortValue = level; + } + + /// + /// + /// + internal void VolumeEventFromChassis() + { + VolumeLevelFeedback.FireUpdate(); + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// + /// + public void MuteToggle() + { + if (IsMuted) + MuteOff(); + else + MuteOn(); + } + + /// + /// + /// + public void VolumeDown(bool pressRelease) + { + if (pressRelease) + Output.Volume.CreateRamp(0, 400); +#warning SCALE THIS RAMP + else + Output.Volume.StopRamp(); + } + + /// + /// + /// + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + Output.Volume.CreateRamp(65535, 400); + else + Output.Volume.StopRamp(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_LOCAL_14408.cs b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_LOCAL_14408.cs new file mode 100644 index 00000000..4b4a927e --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_LOCAL_14408.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + public class DmCardAudioOutputController : IBasicVolumeWithFeedback + { + public Audio.Output Output { get; private set; } + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + ushort PreMuteVolumeLevel; + bool IsMuted; + + public DmCardAudioOutputController(Audio.Output output) + { + Output = output; + VolumeLevelFeedback = new IntFeedback(() => Output.VolumeFeedback.UShortValue); + MuteFeedback = new BoolFeedback(() => IsMuted); + } + + #region IBasicVolumeWithFeedback Members + + /// + /// + /// + public void MuteOff() + { + SetVolume(PreMuteVolumeLevel); + IsMuted = false; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void MuteOn() + { + PreMuteVolumeLevel = Output.VolumeFeedback.UShortValue; + SetVolume(0); + IsMuted = true; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void SetVolume(ushort level) + { + Debug.Console(2, "Set volume out {0}", level); + Output.Volume.UShortValue = level; + } + + /// + /// + /// + internal void VolumeEventFromChassis() + { + VolumeLevelFeedback.FireUpdate(); + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// + /// + public void MuteToggle() + { + if (IsMuted) + MuteOff(); + else + MuteOn(); + } + + /// + /// + /// + public void VolumeDown(bool pressRelease) + { + if (pressRelease) + { + var remainingRatio = Output.Volume.UShortValue / 65535; + Output.Volume.CreateRamp(0, (uint)(400 * remainingRatio)); + } + else + Output.Volume.StopRamp(); + } + + /// + /// + /// + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + { + var remainingRatio = (65535 - Output.Volume.UShortValue) / 65535; + Output.Volume.CreateRamp(65535, 400); + } + else + Output.Volume.StopRamp(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_REMOTE_14408.cs b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_REMOTE_14408.cs new file mode 100644 index 00000000..6c5c0275 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmCardAudioOutput_REMOTE_14408.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + public class DmCardAudioOutputController : IBasicVolumeWithFeedback + { + public Audio.Output Output { get; private set; } + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + ushort PreMuteVolumeLevel; + bool IsMuted; + + public DmCardAudioOutputController(Audio.Output output) + { + Output = output; + VolumeLevelFeedback = new IntFeedback(() => Output.VolumeFeedback.UShortValue); + MuteFeedback = new BoolFeedback(() => IsMuted); + } + + #region IBasicVolumeWithFeedback Members + + /// + /// + /// + public void MuteOff() + { + SetVolume(PreMuteVolumeLevel); + IsMuted = false; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void MuteOn() + { + PreMuteVolumeLevel = Output.VolumeFeedback.UShortValue; + SetVolume(0); + IsMuted = true; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public void SetVolume(ushort level) + { + Debug.Console(2, "Set volume out {0}", level); + Output.Volume.UShortValue = level; + } + + /// + /// + /// + internal void VolumeEventFromChassis() + { + VolumeLevelFeedback.FireUpdate(); + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// + /// + public void MuteToggle() + { + if (IsMuted) + MuteOff(); + else + MuteOn(); + } + + /// + /// + /// + public void VolumeDown(bool pressRelease) + { + if (pressRelease) + Output.Volume.CreateRamp(0, 400); + else + Output.Volume.StopRamp(); + } + + /// + /// + /// + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + Output.Volume.CreateRamp(65535, 400); + else + Output.Volume.StopRamp(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmChassisController.cs b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmChassisController.cs new file mode 100644 index 00000000..4e413749 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Chassis/DmChassisController.cs @@ -0,0 +1,808 @@ + using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Cards; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Cards; + +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmChassisController : CrestronGenericBaseDevice, IRoutingInputsOutputs, IRouting, IHasFeedback//, ICardPortsDevice + { + public DmMDMnxn Chassis { get; private set; } + + // Feedbacks for EssentialDM + public Dictionary VideoOutputFeedbacks { get; private set; } + public Dictionary AudioOutputFeedbacks { get; private set; } + public Dictionary VideoInputSyncFeedbacks { get; private set; } + public Dictionary InputEndpointOnlineFeedbacks { get; private set; } + public Dictionary OutputEndpointOnlineFeedbacks { get; private set; } + public Dictionary InputNameFeedbacks { get; private set; } + public Dictionary OutputNameFeedbacks { get; private set; } + public Dictionary OutputVideoRouteNameFeedbacks { get; private set; } + public Dictionary OutputAudioRouteNameFeedbacks { get; private set; } + + + // Need a couple Lists of generic Backplane ports + public RoutingPortCollection InputPorts { get; private set; } + public RoutingPortCollection OutputPorts { get; private set; } + + public Dictionary TxDictionary { get; set; } + public Dictionary RxDictionary { get; set; } + + //public Dictionary InputCards { get; private set; } + //public Dictionary OutputCards { get; private set; } + + public Dictionary InputNames { get; set; } + public Dictionary OutputNames { get; set; } + public Dictionary VolumeControls { get; private set; } + + public const int RouteOffTime = 500; + Dictionary RouteOffTimers = new Dictionary(); + + /// + /// Factory method to create a new chassis controller from config data. Limited to 8x8 right now + /// + public static DmChassisController GetDmChassisController(string key, string name, + string type, DMChassisPropertiesConfig properties) + { + try + { + type = type.ToLower(); + uint ipid = properties.Control.IpIdInt; + + DmMDMnxn chassis = null; + if (type == "dmmd8x8") { chassis = new DmMd8x8(ipid, Global.ControlSystem); } + else if (type == "dmmd8x8rps") { chassis = new DmMd8x8rps(ipid, Global.ControlSystem); } + else if (type == "dmmd8x8cpu3") { chassis = new DmMd8x8Cpu3(ipid, Global.ControlSystem); } + else if (type == "dmmd8x8cpu3rps") { chassis = new DmMd8x8Cpu3rps(ipid, Global.ControlSystem); } + + else if (type == "dmmd16x16") { chassis = new DmMd16x16(ipid, Global.ControlSystem); } + else if (type == "dmmd16x16rps") { chassis = new DmMd16x16rps(ipid, Global.ControlSystem); } + else if (type == "dmmd16x16cpu3") { chassis = new DmMd16x16Cpu3(ipid, Global.ControlSystem); } + else if (type == "dmmd16x16cpu3rps") { chassis = new DmMd16x16Cpu3rps(ipid, Global.ControlSystem); } + + else if (type == "dmmd32x32") { chassis = new DmMd32x32(ipid, Global.ControlSystem); } + else if (type == "dmmd32x32rps") { chassis = new DmMd32x32rps(ipid, Global.ControlSystem); } + else if (type == "dmmd32x32cpu3") { chassis = new DmMd32x32Cpu3(ipid, Global.ControlSystem); } + else if (type == "dmmd32x32cpu3rps") { chassis = new DmMd32x32Cpu3rps(ipid, Global.ControlSystem); } + + if (chassis == null) + { + return null; + } + + var controller = new DmChassisController(key, name, chassis); + // add the cards and port names + foreach (var kvp in properties.InputSlots) + controller.AddInputCard(kvp.Value, kvp.Key); + foreach (var kvp in properties.OutputSlots) + { + controller.AddOutputCard(kvp.Value, kvp.Key); + } + + foreach (var kvp in properties.VolumeControls) + { + // get the card + // check it for an audio-compatible type + // make a something-something that will make it work + // retire to mountain village + var outNum = kvp.Key; + var card = controller.Chassis.Outputs[outNum].Card; + Audio.Output audio = null; + if (card is DmcHdo) + audio = (card as DmcHdo).Audio; + else if (card is Dmc4kHdo) + audio = (card as Dmc4kHdo).Audio; + if (audio == null) + continue; + // wire up the audio to something here... + controller.AddVolumeControl(outNum, audio); + } + + controller.InputNames = properties.InputNames; + controller.OutputNames = properties.OutputNames; + return controller; + + + //DmChassisController controller = null; + + //if (type == "dmmd8x8") + //{ + // controller = new DmChassisController(key, name, new DmMd8x8(ipid, Global.ControlSystem)); + + // // add the cards and port names + // foreach (var kvp in properties.InputSlots) + // controller.AddInputCard(kvp.Value, kvp.Key); + // foreach (var kvp in properties.OutputSlots) + // { + // controller.AddOutputCard(kvp.Value, kvp.Key); + + // } + + // foreach (var kvp in properties.VolumeControls) + // { + // // get the card + // // check it for an audio-compatible type + // // make a something-something that will make it work + // // retire to mountain village + // var outNum = kvp.Key; + // var card = controller.Chassis.Outputs[outNum].Card; + // Audio.Output audio = null; + // if (card is DmcHdo) + // audio = (card as DmcHdo).Audio; + // else if (card is Dmc4kHdo) + // audio = (card as Dmc4kHdo).Audio; + // if (audio == null) + // continue; + // // wire up the audio to something here... + // controller.AddVolumeControl(outNum, audio); + // } + + // controller.InputNames = properties.InputNames; + // controller.OutputNames = properties.OutputNames; + // return controller; + //} + //else if (type == "dmmd16x16") + //{ + // controller = new DmChassisController(key, name, new DmMd16x16(ipid, Global.ControlSystem)); + + // // add the cards and port names + // foreach (var kvp in properties.InputSlots) + // controller.AddInputCard(kvp.Value, kvp.Key); + // foreach (var kvp in properties.OutputSlots) + // { + // controller.AddOutputCard(kvp.Value, kvp.Key); + + // } + + // foreach (var kvp in properties.VolumeControls) + // { + // // get the card + // // check it for an audio-compatible type + // // make a something-something that will make it work + // // retire to mountain village + // var outNum = kvp.Key; + // var card = controller.Chassis.Outputs[outNum].Card; + // Audio.Output audio = null; + // if (card is DmcHdo) + // audio = (card as DmcHdo).Audio; + // else if (card is Dmc4kHdo) + // audio = (card as Dmc4kHdo).Audio; + // if (audio == null) + // continue; + // // wire up the audio to something here... + // controller.AddVolumeControl(outNum, audio); + // } + + // controller.InputNames = properties.InputNames; + // controller.OutputNames = properties.OutputNames; + // return controller; + //} + } + catch (System.Exception e) + { + Debug.Console(0, "Error creating DM chassis:\r{0}", e); + } + return null; + } + + + /// + /// + /// + /// + /// + /// + public DmChassisController(string key, string name, DmMDMnxn chassis) + : base(key, name, chassis) + { + Chassis = chassis; + InputPorts = new RoutingPortCollection(); + OutputPorts = new RoutingPortCollection(); + VolumeControls = new Dictionary(); + TxDictionary = new Dictionary(); + RxDictionary = new Dictionary(); + IsOnline.OutputChange += new EventHandler(IsOnline_OutputChange); + //IsOnline.OutputChange += new EventHandler(this.IsOnline_OutputChange); + Chassis.DMInputChange += new DMInputEventHandler(Chassis_DMInputChange); + //Chassis.DMSystemChange += new DMSystemEventHandler(Chassis_DMSystemChange); + Chassis.DMOutputChange += new DMOutputEventHandler(Chassis_DMOutputChange); + VideoOutputFeedbacks = new Dictionary(); + AudioOutputFeedbacks = new Dictionary(); + VideoInputSyncFeedbacks = new Dictionary(); + InputNameFeedbacks = new Dictionary(); + OutputNameFeedbacks = new Dictionary(); + OutputVideoRouteNameFeedbacks = new Dictionary(); + OutputAudioRouteNameFeedbacks = new Dictionary(); + InputEndpointOnlineFeedbacks = new Dictionary(); + OutputEndpointOnlineFeedbacks = new Dictionary(); + + + for (uint x = 1; x <= Chassis.NumberOfOutputs; x++) + { + var tempX = x; + + VideoOutputFeedbacks[tempX] = new IntFeedback(() => { + if (Chassis.Outputs[tempX].VideoOutFeedback != null) { return (ushort)Chassis.Outputs[tempX].VideoOutFeedback.Number;} + else { return 0; }; + }); + AudioOutputFeedbacks[tempX] = new IntFeedback(() => { + if (Chassis.Outputs[tempX].AudioOutFeedback != null) { return (ushort)Chassis.Outputs[tempX].AudioOutFeedback.Number; } + else { return 0; }; + }); + VideoInputSyncFeedbacks[tempX] = new BoolFeedback(() => { + return Chassis.Inputs[tempX].VideoDetectedFeedback.BoolValue; + }); + InputNameFeedbacks[tempX] = new StringFeedback(() => { + if (Chassis.Inputs[tempX].NameFeedback.StringValue != null) + { + return Chassis.Inputs[tempX].NameFeedback.StringValue; + } + else + { + return ""; + } + }); + OutputNameFeedbacks[tempX] = new StringFeedback(() => { + if (Chassis.Outputs[tempX].NameFeedback.StringValue != null) + { + return Chassis.Outputs[tempX].NameFeedback.StringValue; + } + else + { + return ""; + } + }); + OutputVideoRouteNameFeedbacks[tempX] = new StringFeedback(() => + { + if (Chassis.Outputs[tempX].VideoOutFeedback != null) + { + return Chassis.Outputs[tempX].VideoOutFeedback.NameFeedback.StringValue; + } + else + { + return ""; + } + }); + OutputAudioRouteNameFeedbacks[tempX] = new StringFeedback(() => + { + if (Chassis.Outputs[tempX].AudioOutFeedback != null) + { + return Chassis.Outputs[tempX].AudioOutFeedback.NameFeedback.StringValue; + } + else + { + return ""; + + } + }); + InputEndpointOnlineFeedbacks[tempX] = new BoolFeedback(() => { return Chassis.Inputs[tempX].EndpointOnlineFeedback; }); + + OutputEndpointOnlineFeedbacks[tempX] = new BoolFeedback(() => { return Chassis.Outputs[tempX].EndpointOnlineFeedback; }); + } + } + + /// + /// + /// + /// + /// + public void AddInputCard(string type, uint number) + { + Debug.Console(2, this, "Adding input card '{0}', slot {1}", type, number); + + if (type == "dmcHd") + { + var inputCard = new DmcHd(number, this.Chassis); + var cecPort = inputCard.HdmiInput as ICec; + AddHdmiInCardPorts(number, cecPort); + } + else if (type == "dmcHdDsp") + { + var inputCard = new DmcHdDsp(number, this.Chassis); + var cecPort = inputCard.HdmiInput as ICec; + AddHdmiInCardPorts(number, cecPort); + } + else if (type == "dmc4kHd") + { + var inputCard = new Dmc4kHd(number, this.Chassis); + var cecPort = inputCard.HdmiInput as ICec; + AddHdmiInCardPorts(number, cecPort); + } + else if (type == "dmc4kHdDsp") + { + var inputCard = new Dmc4kHdDsp(number, this.Chassis); + var cecPort = inputCard.HdmiInput as ICec; + AddHdmiInCardPorts(number, cecPort); + } + else if (type == "dmc4kzHd") + { + var inputCard = new Dmc4kzHd(number, this.Chassis); + var cecPort = inputCard.HdmiInput as ICec; + AddHdmiInCardPorts(number, cecPort); + } + else if (type == "dmc4kzHdDsp") + { + var inputCard = new Dmc4kzHdDsp(number, this.Chassis); + var cecPort = inputCard.HdmiInput as ICec; + AddHdmiInCardPorts(number, cecPort); + } + else if (type == "dmcC") + { + new DmcC(number, this.Chassis); + AddDmInCardPorts(number); + } + else if (type == "dmcCDsp") + { + new DmcCDsp(number, this.Chassis); + AddDmInCardPorts(number); + } + else if (type == "dmc4kC") + { + new Dmc4kC(number, this.Chassis); + AddDmInCardPorts(number); + } + else if (type == "dmc4kCDsp") + { + new Dmc4kCDsp(number, this.Chassis); + AddDmInCardPorts(number); + } + else if (type == "dmc4kzC") + { + new Dmc4kzC(number, this.Chassis); + AddDmInCardPorts(number); + } + else if (type == "dmc4kzCDsp") + { + new Dmc4kzCDsp(number, this.Chassis); + AddDmInCardPorts(number); + } + else if (type == "dmcCat") + { + new DmcCat(number, this.Chassis); + AddDmInCardPorts(number); + } + else if (type == "dmcCatDsp") + { + new DmcCatDsp(number, this.Chassis); + AddDmInCardPorts(number); + } + else if (type == "dmcS") + { + new DmcS(number, Chassis); + AddInputPortWithDebug(number, "dmIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmMmFiber); + AddInCardHdmiAndAudioLoopPorts(number); + } + else if (type == "dmcSDsp") + { + new DmcSDsp(number, Chassis); + AddInputPortWithDebug(number, "dmIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmMmFiber); + AddInCardHdmiAndAudioLoopPorts(number); + } + else if (type == "dmcS2") + { + new DmcS2(number, Chassis); + AddInputPortWithDebug(number, "dmIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmSmFiber); + AddInCardHdmiAndAudioLoopPorts(number); + } + else if (type == "dmcS2Dsp") + { + new DmcS2Dsp(number, Chassis); + AddInputPortWithDebug(number, "dmIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmSmFiber); + AddInCardHdmiAndAudioLoopPorts(number); + } + else if (type == "dmcSdi") + { + new DmcSdi(number, Chassis); + AddInputPortWithDebug(number, "sdiIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Sdi); + AddOutputPortWithDebug(number, "sdiOut", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Sdi, null); + AddInCardHdmiAndAudioLoopPorts(number); + } + else if (type == "dmcDvi") + { + new DmcDvi(number, Chassis); + AddInputPortWithDebug(number, "dviIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Dvi); + AddInputPortWithDebug(number, "audioIn", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio); + AddInCardHdmiLoopPort(number); + } + else if (type == "dmcVga") + { + new DmcVga(number, Chassis); + AddInputPortWithDebug(number, "vgaIn", eRoutingSignalType.Video, eRoutingPortConnectionType.Vga); + AddInputPortWithDebug(number, "audioIn", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio); + AddInCardHdmiLoopPort(number); + } + else if (type == "dmcVidBnc") + { + new DmcVidBnc(number, Chassis); + AddInputPortWithDebug(number, "componentIn", eRoutingSignalType.Video, eRoutingPortConnectionType.Component); + AddInputPortWithDebug(number, "audioIn", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio); + AddInCardHdmiLoopPort(number); + } + else if (type == "dmcVidRcaA") + { + new DmcVidRcaA(number, Chassis); + AddInputPortWithDebug(number, "componentIn", eRoutingSignalType.Video, eRoutingPortConnectionType.Component); + AddInputPortWithDebug(number, "audioIn", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio); + AddInCardHdmiLoopPort(number); + } + else if (type == "dmcVidRcaD") + { + new DmcVidRcaD(number, Chassis); + AddInputPortWithDebug(number, "componentIn", eRoutingSignalType.Video, eRoutingPortConnectionType.Component); + AddInputPortWithDebug(number, "audioIn", eRoutingSignalType.Audio, eRoutingPortConnectionType.DigitalAudio); + AddInCardHdmiLoopPort(number); + } + else if (type == "dmcVid4") + { + new DmcVid4(number, Chassis); + AddInputPortWithDebug(number, "compositeIn1", eRoutingSignalType.Video, eRoutingPortConnectionType.Composite); + AddInputPortWithDebug(number, "compositeIn2", eRoutingSignalType.Video, eRoutingPortConnectionType.Composite); + AddInputPortWithDebug(number, "compositeIn3", eRoutingSignalType.Video, eRoutingPortConnectionType.Composite); + AddInputPortWithDebug(number, "compositeIn4", eRoutingSignalType.Video, eRoutingPortConnectionType.Composite); + AddInCardHdmiLoopPort(number); + } + else if (type == "dmcStr") + { + new DmcStr(number, Chassis); + AddInputPortWithDebug(number, "streamIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Streaming); + AddInCardHdmiAndAudioLoopPorts(number); + } + } + + void AddDmInCardPorts(uint number) + { + AddInputPortWithDebug(number, "dmIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmCat); + AddInCardHdmiAndAudioLoopPorts(number); + } + + void AddHdmiInCardPorts(uint number, ICec cecPort) + { + AddInputPortWithDebug(number, "hdmiIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, cecPort); + AddInCardHdmiAndAudioLoopPorts(number); + } + + void AddInCardHdmiAndAudioLoopPorts(uint number) + { + AddOutputPortWithDebug(number, "hdmiLoopOut", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null); + AddOutputPortWithDebug(number, "audioLoopOut", eRoutingSignalType.Audio, eRoutingPortConnectionType.Hdmi, null); + } + + void AddInCardHdmiLoopPort(uint number) + { + AddOutputPortWithDebug(number, "hdmiLoopOut", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null); + } + + /// + /// + /// + /// + /// + public void AddOutputCard(string type, uint number) + { + Debug.Console(2, this, "Adding output card '{0}', slot {1}", type, number); + if (type == "dmc4kHdo") + { + var outputCard = new Dmc4kHdoSingle(number, Chassis); + var cecPort1 = outputCard.Card1.HdmiOutput; + var cecPort2 = outputCard.Card2.HdmiOutput; + AddDmcHdoPorts(number, cecPort1, cecPort2); + } + else if (type == "dmcHdo") + { + var outputCard = new DmcHdoSingle(number, Chassis); + var cecPort1 = outputCard.Card1.HdmiOutput; + var cecPort2 = outputCard.Card2.HdmiOutput; + AddDmcHdoPorts(number, cecPort1, cecPort2); + } + else if (type == "dmc4kCoHd") + { + var outputCard = new Dmc4kCoHdSingle(number, Chassis); + var cecPort1 = outputCard.Card1.HdmiOutput; + AddDmcCoPorts(number, cecPort1); + } + else if (type == "dmc4kzCoHd") + { + var outputCard = new Dmc4kzCoHdSingle(number, Chassis); + var cecPort1 = outputCard.Card1.HdmiOutput; + AddDmcCoPorts(number, cecPort1); + } + else if (type == "dmcCoHd") + { + var outputCard = new DmcCoHdSingle(number, Chassis); + var cecPort1 = outputCard.Card1.HdmiOutput; + AddDmcCoPorts(number, cecPort1); + } + else if (type == "dmCatoHd") + { + var outputCard = new DmcCatoHdSingle(number, Chassis); + var cecPort1 = outputCard.Card1.HdmiOutput; + AddDmcCoPorts(number, cecPort1); + } + else if (type == "dmcSoHd") + { + var outputCard = new DmcSoHdSingle(number, Chassis); + var cecPort1 = outputCard.Card1.HdmiOutput; + AddOutputPortWithDebug(number, "dmOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmMmFiber, 2 * (number - 1) + 1); + AddOutputPortWithDebug(number, "hdmiOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2 * (number - 1) + 1, cecPort1); + AddOutputPortWithDebug(number, "dmOut2", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmMmFiber, 2 * (number - 1) + 2); + + } + else if (type == "dmcS2oHd") + { + var outputCard = new DmcS2oHdSingle(number, Chassis); + var cecPort1 = outputCard.Card1.HdmiOutput; + AddOutputPortWithDebug(number, "dmOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmSmFiber, 2 * (number - 1) + 1); + AddOutputPortWithDebug(number, "hdmiOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2 * (number - 1) + 1, cecPort1); + AddOutputPortWithDebug(number, "dmOut2", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmSmFiber, 2 * (number - 1) + 2); + } + else if (type == "dmcStro") + { + var outputCard = new DmcStroSingle(number, Chassis); + AddOutputPortWithDebug(number, "streamOut", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Streaming, 2 * (number - 1) + 1); + } + + else + Debug.Console(1, this, " WARNING: Output card type '{0}' is not available", type); + } + + void AddDmcHdoPorts(uint number, ICec cecPort1, ICec cecPort2) + { + AddOutputPortWithDebug(number, "hdmiOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2 * (number - 1) + 1, cecPort1); + AddOutputPortWithDebug(number, "audioOut1", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio, 2 * (number - 1) + 1); + AddOutputPortWithDebug(number, "hdmiOut2", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2 * (number - 1) + 2, cecPort2); + AddOutputPortWithDebug(number, "audioOut2", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio, 2 * (number - 1) + 2); + } + + void AddDmcCoPorts(uint number, ICec cecPort1) + { + AddOutputPortWithDebug(number, "dmOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmCat, 2 * (number - 1) + 1); + AddOutputPortWithDebug(number, "hdmiOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2 * (number - 1) + 1, cecPort1); + AddOutputPortWithDebug(number, "dmOut2", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmCat, 2 * (number - 1) + 2); + } + + + /// + /// Adds InputPort + /// + void AddInputPortWithDebug(uint cardNum, string portName, eRoutingSignalType sigType, eRoutingPortConnectionType portType) + { + var portKey = string.Format("inputCard{0}--{1}", cardNum, portName); + Debug.Console(2, this, "Adding input port '{0}'", portKey); + var inputPort = new RoutingInputPort(portKey, sigType, portType, cardNum, this); + + InputPorts.Add(inputPort); + } + + /// + /// Adds InputPort and sets Port as ICec object + /// + void AddInputPortWithDebug(uint cardNum, string portName, eRoutingSignalType sigType, eRoutingPortConnectionType portType, ICec cecPort) + { + var portKey = string.Format("inputCard{0}--{1}", cardNum, portName); + Debug.Console(2, this, "Adding input port '{0}'", portKey); + var inputPort = new RoutingInputPort(portKey, sigType, portType, cardNum, this); + + if (cecPort != null) + inputPort.Port = cecPort; + + InputPorts.Add(inputPort); + } + + /// + /// Adds OutputPort + /// + void AddOutputPortWithDebug(uint cardNum, string portName, eRoutingSignalType sigType, eRoutingPortConnectionType portType, object selector) + { + var portKey = string.Format("outputCard{0}--{1}", cardNum, portName); + Debug.Console(2, this, "Adding output port '{0}'", portKey); + OutputPorts.Add(new RoutingOutputPort(portKey, sigType, portType, selector, this)); + } + + /// + /// Adds OutputPort and sets Port as ICec object + /// + void AddOutputPortWithDebug(uint cardNum, string portName, eRoutingSignalType sigType, eRoutingPortConnectionType portType, object selector, ICec cecPort) + { + var portKey = string.Format("outputCard{0}--{1}", cardNum, portName); + Debug.Console(2, this, "Adding output port '{0}'", portKey); + var outputPort = new RoutingOutputPort(portKey, sigType, portType, selector, this); + + if (cecPort != null) + outputPort.Port = cecPort; + + OutputPorts.Add(outputPort); + } + + /// + /// + /// + void AddVolumeControl(uint number, Audio.Output audio) + { + VolumeControls.Add(number, new DmCardAudioOutputController(audio)); + } + + //public void SetInputHdcpSupport(uint input, ePdtHdcpSupport hdcpSetting) + //{ + + //} + + + void Chassis_DMSystemChange(Switch device, DMSystemEventArgs args) { + + } + void Chassis_DMInputChange(Switch device, DMInputEventArgs args) { + //Debug.Console(2, this, "DMSwitch:{0} Input:{1} Event:{2}'", this.Name, args.Number, args.EventId.ToString()); + + switch (args.EventId) { + case (DMInputEventIds.OnlineFeedbackEventId): { + Debug.Console(2, this, "DMINput OnlineFeedbackEventId for input: {0}. State: {1}", args.Number, device.Inputs[args.Number].EndpointOnlineFeedback); + InputEndpointOnlineFeedbacks[args.Number].FireUpdate(); + break; + } + case (DMInputEventIds.VideoDetectedEventId): { + Debug.Console(2, this, "DM Input {0} VideoDetectedEventId", args.Number); + VideoInputSyncFeedbacks[args.Number].FireUpdate(); + break; + } + case (DMInputEventIds.InputNameEventId): { + Debug.Console(2, this, "DM Input {0} NameFeedbackEventId", args.Number); + InputNameFeedbacks[args.Number].FireUpdate(); + break; + } + } + } + /// + /// + void Chassis_DMOutputChange(Switch device, DMOutputEventArgs args) + { + + //This should be a switch case JTA 2018-07-02 + var output = args.Number; + if (args.EventId == DMOutputEventIds.VolumeEventId && + VolumeControls.ContainsKey(output)) + { + VolumeControls[args.Number].VolumeEventFromChassis(); + } + else if (args.EventId == DMOutputEventIds.OnlineFeedbackEventId) + { + OutputEndpointOnlineFeedbacks[output].FireUpdate(); + } + else if (args.EventId == DMOutputEventIds.VideoOutEventId) + { + if (Chassis.Outputs[output].VideoOutFeedback != null) + { + Debug.Console(2, this, "DMSwitchVideo:{0} Routed Input:{1} Output:{2}'", this.Name, Chassis.Outputs[output].VideoOutFeedback.Number, output); + } + if (VideoOutputFeedbacks.ContainsKey(output)) + { + VideoOutputFeedbacks[output].FireUpdate(); + + } + if (OutputVideoRouteNameFeedbacks.ContainsKey(output)) + { + OutputVideoRouteNameFeedbacks[output].FireUpdate(); + } + } + else if (args.EventId == DMOutputEventIds.AudioOutEventId) + { + if (Chassis.Outputs[output].AudioOutFeedback != null) + { + Debug.Console(2, this, "DMSwitchAudio:{0} Routed Input:{1} Output:{2}'", this.Name, Chassis.Outputs[output].AudioOutFeedback.Number, output); + } + if (AudioOutputFeedbacks.ContainsKey(output)) + { + AudioOutputFeedbacks[output].FireUpdate(); + } + } + else if (args.EventId == DMOutputEventIds.OutputNameEventId) + { + Debug.Console(2, this, "DM Output {0} NameFeedbackEventId", output); + OutputNameFeedbacks[output].FireUpdate(); + } + + } + + /// + /// + /// + /// + void StartOffTimer(PortNumberType pnt) + { + if (RouteOffTimers.ContainsKey(pnt)) + return; + RouteOffTimers[pnt] = new CTimer(o => + { + ExecuteSwitch(0, pnt.Number, pnt.Type); + }, RouteOffTime); + } + + + // Send out sigs when coming online + void IsOnline_OutputChange(object sender, EventArgs e) + { + if (IsOnline.BoolValue) + { + Chassis.EnableAudioBreakaway.BoolValue = true; + //Chassis.EnableUSBBreakaway.BoolValue = true; + + if (InputNames != null) + foreach (var kvp in InputNames) + Chassis.Inputs[kvp.Key].Name.StringValue = kvp.Value; + if (OutputNames != null) + foreach(var kvp in OutputNames) + Chassis.Outputs[kvp.Key].Name.StringValue = kvp.Value; + } + } + + #region IRouting Members + + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType sigType) + { + Debug.Console(2, this, "Making an awesome DM route from {0} to {1} {2}", inputSelector, outputSelector, sigType); + + var input = Convert.ToUInt32(inputSelector); // Cast can sometimes fail + var output = Convert.ToUInt32(outputSelector); + // Check to see if there's an off timer waiting on this and if so, cancel + var key = new PortNumberType(output, sigType); + if (input == 0) + { + StartOffTimer(key); + } + else + { + if(RouteOffTimers.ContainsKey(key)) + { + Debug.Console(2, this, "{0} cancelling route off due to new source", output); + RouteOffTimers[key].Stop(); + RouteOffTimers.Remove(key); + } + } + + Card.DMICard inCard = input == 0 ? null : Chassis.Inputs[input]; + + // NOTE THAT THESE ARE NOTS - TO CATCH THE AudioVideo TYPE + if (sigType != eRoutingSignalType.Audio) + { + Chassis.VideoEnter.BoolValue = true; + Chassis.Outputs[output].VideoOut = inCard; + } + + if (sigType != eRoutingSignalType.Video) + { + Chassis.AudioEnter.BoolValue = true; + Chassis.Outputs[output].AudioOut = inCard; + } + } + + #endregion + + } + + public struct PortNumberType + { + public uint Number { get; private set; } + public eRoutingSignalType Type { get; private set; } + + public PortNumberType(uint number, eRoutingSignalType type) : this() + { + Number = number; + Type = type; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/HdMdNxM4kEController.cs b/essentials-framework/Essentials DM/Essentials_DM/Chassis/HdMdNxM4kEController.cs new file mode 100644 index 00000000..e95cd5bd --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Chassis/HdMdNxM4kEController.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM.Chassis +{ + public class HdMdNxM4kEController : Device, IRoutingInputsOutputs, IRouting + { + public HdMdNxM Chassis { get; private set; } + + public RoutingPortCollection InputPorts { get; private set; } + public RoutingPortCollection OutputPorts { get; private set; } + + + /// + /// + /// + /// + /// + /// + public HdMdNxM4kEController(string key, string name, HdMdNxM chassis, + HdMdNxM4kEPropertiesConfig props) + : base(key, name) + { + Chassis = chassis; + + // logical ports + InputPorts = new RoutingPortCollection(); + for (uint i = 1; i <= 4; i++) + { + InputPorts.Add(new RoutingInputPort("hdmiIn" + i, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, i, this)); + } + OutputPorts = new RoutingPortCollection(); + OutputPorts.Add(new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this)); + + // physical settings + if (props != null && props.Inputs != null) + { + foreach (var kvp in props.Inputs) + { + // strip "hdmiIn" + var inputNum = Convert.ToUInt32(kvp.Key.Substring(6)); + + var port = chassis.HdmiInputs[inputNum].HdmiInputPort; + // set hdcp disables + if (kvp.Value.DisableHdcp) + { + Debug.Console(0, this, "Configuration disables HDCP support on {0}", kvp.Key); + port.HdcpSupportOff(); + } + else + port.HdcpSupportOn(); + } + } + } + + public override bool CustomActivate() + { + var result = Chassis.Register(); + if (result != Crestron.SimplSharpPro.eDeviceRegistrationUnRegistrationResponse.Success) + { + Debug.Console(0, this, "Device registration failed: {0}", result); + return false; + } + + return base.CustomActivate(); + } + + + + #region IRouting Members + + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) + { + // Try to make switch only when necessary. The unit appears to toggle when already selected. + var current = Chassis.HdmiOutputs[1].VideoOut; + if(current != Chassis.HdmiInputs[(uint)inputSelector]) + Chassis.HdmiOutputs[1].VideoOut = Chassis.HdmiInputs[(uint)inputSelector]; + } + + #endregion + + ///////////////////////////////////////////////////// + + /// + /// + /// + /// + /// + /// + /// + /// + public static HdMdNxM4kEController GetController(string key, string name, + string type, HdMdNxM4kEPropertiesConfig properties) + { + try + { + var ipid = properties.Control.IpIdInt; + var address = properties.Control.TcpSshProperties.Address; + + type = type.ToLower(); + if (type == "hdmd4x14ke") + { + var chassis = new HdMd4x14kE(ipid, address, Global.ControlSystem); + return new HdMdNxM4kEController(key, name, chassis, properties); + } + return null; + } + catch (Exception e) + { + Debug.Console(0, "ERROR Creating device key {0}: \r{1}", key, e); + return null; + } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Config/DMChassisConfig.cs b/essentials-framework/Essentials DM/Essentials_DM/Config/DMChassisConfig.cs new file mode 100644 index 00000000..81f28afa --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Config/DMChassisConfig.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharpPro.DM; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Cards; + +namespace PepperDash.Essentials.DM.Config +{ + /// + /// Represents the "properties" property of a DM device config + /// + public class DMChassisPropertiesConfig + { + [JsonProperty("control")] + public ControlPropertiesConfig Control { get; set; } + + [JsonProperty("volumeControls")] + public Dictionary VolumeControls { get; set; } + + [JsonProperty("inputSlots")] + public Dictionary InputSlots { get; set; } + + [JsonProperty("outputSlots")] + public Dictionary OutputSlots { get; set; } + + [JsonProperty("inputNames")] + public Dictionary InputNames { get; set; } + + [JsonProperty("outputNames")] + public Dictionary OutputNames { get; set; } + } + + /// + /// + /// + public class DmCardAudioPropertiesConfig + { + [JsonProperty("outLevel")] + public int OutLevel { get; set; } + + [JsonProperty("isVolumeControlPoint")] + public bool IsVolumeControlPoint { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Config/DeviceFactory.cs b/essentials-framework/Essentials DM/Essentials_DM/Config/DeviceFactory.cs new file mode 100644 index 00000000..f425843f --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Config/DeviceFactory.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM.AirMedia; +using Crestron.SimplSharpPro.UI; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.DM.AirMedia; +using PepperDash.Essentials.DM.Endpoints.DGEs; + +namespace PepperDash.Essentials.DM +{ + public class DeviceFactory + { + public static IKeyed GetDevice(DeviceConfig dc) + { + var key = dc.Key; + var name = dc.Name; + var type = dc.Type; + var properties = dc.Properties; + + var typeName = dc.Type.ToLower(); + + if (typeName.StartsWith("am")) + { + var props = JsonConvert.DeserializeObject(properties.ToString()); + AmX00 amDevice = null; + if (typeName == "am200") + amDevice = new Crestron.SimplSharpPro.DM.AirMedia.Am200(props.Control.IpIdInt, Global.ControlSystem); + else if(typeName == "am300") + amDevice = new Crestron.SimplSharpPro.DM.AirMedia.Am300(props.Control.IpIdInt, Global.ControlSystem); + + return new AirMediaController(key, name, amDevice, dc, props); + } + else if (typeName.StartsWith("dmmd8x") || typeName.StartsWith("dmmd16x") || typeName.StartsWith("dmmd32x")) + { + var props = JsonConvert.DeserializeObject + (properties.ToString()); + return PepperDash.Essentials.DM.DmChassisController. + GetDmChassisController(key, name, type, props); + } + // Hand off to DmTxHelper class + else if (typeName.StartsWith("dmtx")) + { + var props = JsonConvert.DeserializeObject + (properties.ToString()); + return PepperDash.Essentials.DM.DmTxHelper.GetDmTxController(key, name, type, props); + } + + // Hand off to DmRmcHelper class + else if (typeName.StartsWith("dmrmc")) + { + var props = JsonConvert.DeserializeObject + (properties.ToString()); + return PepperDash.Essentials.DM.DmRmcHelper.GetDmRmcController(key, name, type, props); + } + + else if (typeName.Equals("hdmd4x14ke")) + { + var props = JsonConvert.DeserializeObject + (properties.ToString()); + return PepperDash.Essentials.DM.Chassis.HdMdNxM4kEController.GetController(key, name, type, props); + } + + + + return null; + } + + + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Config/DmRmcConfig.cs b/essentials-framework/Essentials DM/Essentials_DM/Config/DmRmcConfig.cs new file mode 100644 index 00000000..702e10b5 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Config/DmRmcConfig.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharpPro.DM; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using PepperDash.Core; +using PepperDash.Essentials.DM.Cards; + +namespace PepperDash.Essentials.DM.Config +{ + /// + /// Represents the "properties" property of a DM TX device config + /// + public class DmRmcPropertiesConfig + { + [JsonProperty("control")] + public ControlPropertiesConfig Control { get; set; } + + [JsonProperty("parentDeviceKey")] + public string ParentDeviceKey { get; set; } + + [JsonProperty("parentOutputNumber")] + public uint ParentOutputNumber { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Config/DmTxConfig.cs b/essentials-framework/Essentials DM/Essentials_DM/Config/DmTxConfig.cs new file mode 100644 index 00000000..51d074f8 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Config/DmTxConfig.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharpPro.DM; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using PepperDash.Core; +using PepperDash.Essentials.DM.Cards; + +namespace PepperDash.Essentials.DM.Config +{ + /// + /// Represents the "properties" property of a DM TX device config + /// + public class DmTxPropertiesConfig + { + [JsonProperty("control")] + public ControlPropertiesConfig Control { get; set; } + + [JsonProperty("parentDeviceKey")] + public string ParentDeviceKey { get; set; } + + [JsonProperty("parentInputNumber")] + public uint ParentInputNumber { get; set; } + + [JsonProperty("autoSwitching")] + public bool AutoSwitching { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Config/HdMdNxM4kEPropertiesConfig.cs b/essentials-framework/Essentials DM/Essentials_DM/Config/HdMdNxM4kEPropertiesConfig.cs new file mode 100644 index 00000000..06411e39 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Config/HdMdNxM4kEPropertiesConfig.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Newtonsoft.Json; + +using PepperDash.Core; + +namespace PepperDash.Essentials.DM.Config +{ + /// + /// Defines the properties section of HdMdNxM boxes + /// + public class HdMdNxM4kEPropertiesConfig + { + [JsonProperty("control")] + public ControlPropertiesConfig Control { get; set; } + + [JsonProperty("inputs")] + public Dictionary Inputs { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Config/InputPropertiesConfig.cs b/essentials-framework/Essentials DM/Essentials_DM/Config/InputPropertiesConfig.cs new file mode 100644 index 00000000..40f958ec --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Config/InputPropertiesConfig.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.DM.Config +{ + public class InputPropertiesConfig + { + public string Name { get; set; } + + public bool DisableHdcp { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/DmPortName.cs b/essentials-framework/Essentials DM/Essentials_DM/DmPortName.cs new file mode 100644 index 00000000..c862f1ed --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/DmPortName.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.DM +{ + /// + /// Constants for consistent port naming + /// + public class DmPortName + { + public const string AirBoardIn = "AirBoardIn"; + public const string AirMediaIn = "AirMediaIn"; + public const string AnyVideoIn = "AnyVideoIn"; + public const string AudioLoopOut = "AudioLoopOut"; + public const string BalancedAudioOut = "BalancedAudioOut"; + public const string BalancedAudioOut1 = "BalancedAudioOut1"; + public const string BalancedAudioOut2 = "BalancedAudioOut2"; + public const string CompositeIn = "CompositeIn"; + public const string DisplayPortIn = "DisplayPortIn"; + public const string DmIn = "DmIn"; + public const string DmOut = "DmOut"; + public const string DmOut1 = "DmOut1"; + public const string DmOut2 = "DmOut2"; + public const string HdmiIn = "HdmiIn"; + public const string HdmiIn1 = "HdmiIn1"; + public const string HdmiIn2 = "HdmiIn2"; + public const string HdmiOut1 = "HdmiOut1"; + public const string HdmiOut2 = "HdmiOut2"; + public const string HdmiLoopOut = "HdmiLoopOut"; + public const string HdmiOut = "HdmiOut"; + public const string Osd = "Osd"; + public const string VgaIn = "VgaIn"; + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/DGEs/DgeController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/DGEs/DgeController.cs new file mode 100644 index 00000000..f8cf0938 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/DGEs/DgeController.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.UI; + +using Newtonsoft.Json; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.DM.Endpoints.DGEs +{ + /// + /// Wrapper class for DGE-100 and DM-DGE-200-C + /// + public class DgeController : CrestronGenericBaseDevice, IComPorts, IIROutputPorts + { + public Dge100 DigitalGraphicsEngine { get; private set; } + + public DeviceConfig DeviceConfig { get; private set; } + + CrestronTouchpanelPropertiesConfig PropertiesConfig; + + public DgeController(string key, string name, Dge100 device, DeviceConfig dc, CrestronTouchpanelPropertiesConfig props) + :base(key, name, device) + { + DeviceConfig = dc; + + PropertiesConfig = props; + } + + #region IComPorts Members + + public CrestronCollection ComPorts + { + get { return DigitalGraphicsEngine.ComPorts; } + } + + public int NumberOfComPorts + { + get { return DigitalGraphicsEngine.NumberOfComPorts; } + } + + #endregion + + #region IIROutputPorts Members + + public CrestronCollection IROutputPorts + { + get { return DigitalGraphicsEngine.IROutputPorts; } + } + + public int NumberOfIROutputPorts + { + get { return DigitalGraphicsEngine.NumberOfIROutputPorts; } + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/DGEs/DgePropertiesConfig.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/DGEs/DgePropertiesConfig.cs new file mode 100644 index 00000000..2b741efa --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/DGEs/DgePropertiesConfig.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + +using Newtonsoft.Json; + +namespace PepperDash.Essentials.DM.Endpoints.DGEs +{ + public class DgePropertiesConfig + { + [JsonProperty("control")] + public ControlPropertiesConfig Control { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvx35xController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvx35xController.cs new file mode 100644 index 00000000..cd12ea04 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvx35xController.cs @@ -0,0 +1,19 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; + +//using Crestron.SimplSharpPro.DM.Streaming; + +//using PepperDash.Core; +//using PepperDash.Essentials.Core; + +//namespace PepperDash.Essentials.DM.Endpoints.NVX +//{ +// public class DmNvx35xController: DmNvxControllerBase +// { + + +// } +//} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvxConfig.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvxConfig.cs new file mode 100644 index 00000000..b4cecc4e --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvxConfig.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DM.Streaming; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using PepperDash.Core; + +namespace PepperDash.Essentials.DM.Endpoints.NVX +{ + /// + /// Represents the "properties" property of a DM NVX device config + /// + public class DmNvxConfig + { + [JsonProperty("control")] + public ControlPropertiesConfig Control { get; set; } + + [JsonProperty("parrentDeviceKey")] + public string ParentDeviceKey { get; set; } + + [JsonProperty("deviceMode")] + public eDeviceMode DeviceMode { get; set; } + + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvxControllerBase.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvxControllerBase.cs new file mode 100644 index 00000000..6063b893 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/NVX/DmNvxControllerBase.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM.Streaming; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM.Endpoints.NVX +{ + public abstract class DmNvxControllerBase: CrestronGenericBaseDevice + { + public DmNvx35x DmNvx { get; private set; } + + + + public abstract StringFeedback ActiveVideoInputFeedback { get; protected set; } + public RoutingInputPortWithVideoStatuses AnyVideoInput { get; protected set; } + + + public DmNvxControllerBase(string key, string name, DmNvxBaseClass hardware) + : base(key, name, hardware) + { + AddToFeedbackList(ActiveVideoInputFeedback); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmHdBaseTEndpointController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmHdBaseTEndpointController.cs new file mode 100644 index 00000000..e269516f --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmHdBaseTEndpointController.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + public class HDBaseTRxController : DmHdBaseTControllerBase, IRoutingInputsOutputs, + IComPorts + { + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HDBaseTSink { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HDBaseTSink }; } + } + + public HDBaseTRxController(string key, string name, HDRx3CB rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HDBaseTSink = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + // Set Ports for CEC + HDBaseTSink.Port = Rmc; // Unique case, this class has no HdmiOutput port and ICec is implemented on the receiver class itself + } + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc100SController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc100SController.cs new file mode 100644 index 00000000..6019ee75 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc100SController.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmRmc100SController : DmRmcControllerBase, IRoutingInputsOutputs, + IIROutputPorts, IComPorts, ICec + { + public DmRmc100S Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmc100SController(string key, string name, DmRmc100S rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + // Set Ports for CEC + HdmiOut.Port = Rmc; // Unique case, this class has no HdmiOutput port and ICec is implemented on the receiver class itself + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + public Cec StreamCec { get { return Rmc.HdmiOutput.StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc150SController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc150SController.cs new file mode 100644 index 00000000..8deffb19 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc150SController.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmRmc150SController : DmRmcControllerBase, IRoutingInputsOutputs, + IIROutputPorts, IComPorts, ICec + { + public DmRmc150S Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmc150SController(string key, string name, DmRmc150S rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + EdidManufacturerFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Manufacturer.StringValue); + EdidNameFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Name.StringValue); + EdidPreferredTimingFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.PreferredTiming.StringValue); + EdidSerialNumberFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.SerialNumber.StringValue); + + //VideoOutputResolutionFeedback = new StringFeedback(() => Rmc.HdmiOutput.GetVideoResolutionString()); + + //Rmc.HdmiOutput.OutputStreamChange += HdmiOutput_OutputStreamChange; + Rmc.HdmiOutput.ConnectedDevice.DeviceInformationChange += ConnectedDevice_DeviceInformationChange; + + // Set Ports for CEC + HdmiOut.Port = Rmc.HdmiOutput; + } + + //void HdmiOutput_OutputStreamChange(EndpointOutputStream outputStream, EndpointOutputStreamEventArgs args) + //{ + // if (args.EventId == EndpointOutputStreamEventIds.HorizontalResolutionFeedbackEventId || args.EventId == EndpointOutputStreamEventIds.VerticalResolutionFeedbackEventId || + // args.EventId == EndpointOutputStreamEventIds.FramesPerSecondFeedbackEventId) + // { + // VideoOutputResolutionFeedback.FireUpdate(); + // } + //} + + void ConnectedDevice_DeviceInformationChange(ConnectedDeviceInformation connectedDevice, ConnectedDeviceEventArgs args) + { + if (args.EventId == ConnectedDeviceEventIds.ManufacturerEventId) + { + EdidManufacturerFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.NameEventId) + { + EdidNameFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.PreferredTimingEventId) + { + EdidPreferredTimingFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.SerialNumberEventId) + { + EdidSerialNumberFeedback.FireUpdate(); + } + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + public Cec StreamCec { get { return Rmc.HdmiOutput.StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200CController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200CController.cs new file mode 100644 index 00000000..03d00149 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200CController.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmRmc200CController : DmRmcControllerBase, IRoutingInputsOutputs, + IIROutputPorts, IComPorts, ICec + { + public DmRmc200C Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmc200CController(string key, string name, DmRmc200C rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + EdidManufacturerFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Manufacturer.StringValue); + EdidNameFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Name.StringValue); + EdidPreferredTimingFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.PreferredTiming.StringValue); + EdidSerialNumberFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.SerialNumber.StringValue); + + VideoOutputResolutionFeedback = new StringFeedback(() => Rmc.HdmiOutput.GetVideoResolutionString()); + + Rmc.HdmiOutput.OutputStreamChange += HdmiOutput_OutputStreamChange; + Rmc.HdmiOutput.ConnectedDevice.DeviceInformationChange += ConnectedDevice_DeviceInformationChange; + + // Set Ports for CEC + HdmiOut.Port = Rmc.HdmiOutput; + } + + void HdmiOutput_OutputStreamChange(EndpointOutputStream outputStream, EndpointOutputStreamEventArgs args) + { + if (args.EventId == EndpointOutputStreamEventIds.HorizontalResolutionFeedbackEventId || args.EventId == EndpointOutputStreamEventIds.VerticalResolutionFeedbackEventId || + args.EventId == EndpointOutputStreamEventIds.FramesPerSecondFeedbackEventId) + { + VideoOutputResolutionFeedback.FireUpdate(); + } + } + + void ConnectedDevice_DeviceInformationChange(ConnectedDeviceInformation connectedDevice, ConnectedDeviceEventArgs args) + { + if (args.EventId == ConnectedDeviceEventIds.ManufacturerEventId) + { + EdidManufacturerFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.NameEventId) + { + EdidNameFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.PreferredTimingEventId) + { + EdidPreferredTimingFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.SerialNumberEventId) + { + EdidSerialNumberFeedback.FireUpdate(); + } + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + public Cec StreamCec { get { return Rmc.HdmiOutput.StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200S2Controller.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200S2Controller.cs new file mode 100644 index 00000000..11bd5940 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200S2Controller.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmRmc200S2Controller : DmRmcControllerBase, IRoutingInputsOutputs, + IIROutputPorts, IComPorts, ICec + { + public DmRmc200S2 Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmc200S2Controller(string key, string name, DmRmc200S2 rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + EdidManufacturerFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Manufacturer.StringValue); + EdidNameFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Name.StringValue); + EdidPreferredTimingFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.PreferredTiming.StringValue); + EdidSerialNumberFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.SerialNumber.StringValue); + + VideoOutputResolutionFeedback = new StringFeedback(() => Rmc.HdmiOutput.GetVideoResolutionString()); + + Rmc.HdmiOutput.OutputStreamChange += HdmiOutput_OutputStreamChange; + Rmc.HdmiOutput.ConnectedDevice.DeviceInformationChange += ConnectedDevice_DeviceInformationChange; + + // Set Ports for CEC + HdmiOut.Port = Rmc.HdmiOutput; + } + + void HdmiOutput_OutputStreamChange(EndpointOutputStream outputStream, EndpointOutputStreamEventArgs args) + { + if (args.EventId == EndpointOutputStreamEventIds.HorizontalResolutionFeedbackEventId || args.EventId == EndpointOutputStreamEventIds.VerticalResolutionFeedbackEventId || + args.EventId == EndpointOutputStreamEventIds.FramesPerSecondFeedbackEventId) + { + VideoOutputResolutionFeedback.FireUpdate(); + } + } + + void ConnectedDevice_DeviceInformationChange(ConnectedDeviceInformation connectedDevice, ConnectedDeviceEventArgs args) + { + if (args.EventId == ConnectedDeviceEventIds.ManufacturerEventId) + { + EdidManufacturerFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.NameEventId) + { + EdidNameFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.PreferredTimingEventId) + { + EdidPreferredTimingFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.SerialNumberEventId) + { + EdidSerialNumberFeedback.FireUpdate(); + } + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + public Cec StreamCec { get { return Rmc.HdmiOutput.StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200SController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200SController.cs new file mode 100644 index 00000000..ed172723 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc200SController.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmRmc200SController : DmRmcControllerBase, IRoutingInputsOutputs, + IIROutputPorts, IComPorts, ICec + { + public DmRmc200S Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmc200SController(string key, string name, DmRmc200S rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + EdidManufacturerFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Manufacturer.StringValue); + EdidNameFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Name.StringValue); + EdidPreferredTimingFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.PreferredTiming.StringValue); + EdidSerialNumberFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.SerialNumber.StringValue); + + VideoOutputResolutionFeedback = new StringFeedback(() => Rmc.HdmiOutput.GetVideoResolutionString()); + + Rmc.HdmiOutput.OutputStreamChange += HdmiOutput_OutputStreamChange; + Rmc.HdmiOutput.ConnectedDevice.DeviceInformationChange += ConnectedDevice_DeviceInformationChange; + + // Set Ports for CEC + HdmiOut.Port = Rmc.HdmiOutput; + } + + void HdmiOutput_OutputStreamChange(EndpointOutputStream outputStream, EndpointOutputStreamEventArgs args) + { + if (args.EventId == EndpointOutputStreamEventIds.HorizontalResolutionFeedbackEventId || args.EventId == EndpointOutputStreamEventIds.VerticalResolutionFeedbackEventId || + args.EventId == EndpointOutputStreamEventIds.FramesPerSecondFeedbackEventId) + { + VideoOutputResolutionFeedback.FireUpdate(); + } + } + + void ConnectedDevice_DeviceInformationChange(ConnectedDeviceInformation connectedDevice, ConnectedDeviceEventArgs args) + { + if (args.EventId == ConnectedDeviceEventIds.ManufacturerEventId) + { + EdidManufacturerFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.NameEventId) + { + EdidNameFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.PreferredTimingEventId) + { + EdidPreferredTimingFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.SerialNumberEventId) + { + EdidSerialNumberFeedback.FireUpdate(); + } + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + public Cec StreamCec { get { return Rmc.HdmiOutput.StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4KScalerCController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4KScalerCController.cs new file mode 100644 index 00000000..02cb53c0 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4KScalerCController.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmRmc4kScalerCController : DmRmcControllerBase, IRoutingInputsOutputs, IBasicVolumeWithFeedback, + IIROutputPorts, IComPorts, ICec, IRelayPorts + { + public DmRmc4kScalerC Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + public RoutingOutputPort BalancedAudioOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut, BalancedAudioOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmc4kScalerCController(string key, string name, DmRmc4kScalerC rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + BalancedAudioOut = new RoutingOutputPort(DmPortName.BalancedAudioOut, eRoutingSignalType.Audio, + eRoutingPortConnectionType.LineAudio, null, this); + + MuteFeedback = new BoolFeedback(() => false); + VolumeLevelFeedback = new IntFeedback("MainVolumeLevelFeedback", () => + rmc.AudioOutput.VolumeFeedback.UShortValue); + + EdidManufacturerFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Manufacturer.StringValue); + EdidNameFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Name.StringValue); + EdidPreferredTimingFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.PreferredTiming.StringValue); + EdidSerialNumberFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.SerialNumber.StringValue); + + VideoOutputResolutionFeedback = new StringFeedback(() => Rmc.HdmiOutput.GetVideoResolutionString()); + + Rmc.HdmiOutput.OutputStreamChange += HdmiOutput_OutputStreamChange; + Rmc.HdmiOutput.ConnectedDevice.DeviceInformationChange += ConnectedDevice_DeviceInformationChange; + + // Set Ports for CEC + HdmiOut.Port = Rmc.HdmiOutput; + } + + void HdmiOutput_OutputStreamChange(EndpointOutputStream outputStream, EndpointOutputStreamEventArgs args) + { + if (args.EventId == EndpointOutputStreamEventIds.HorizontalResolutionFeedbackEventId || args.EventId == EndpointOutputStreamEventIds.VerticalResolutionFeedbackEventId || + args.EventId == EndpointOutputStreamEventIds.FramesPerSecondFeedbackEventId) + { + VideoOutputResolutionFeedback.FireUpdate(); + } + } + + void ConnectedDevice_DeviceInformationChange(ConnectedDeviceInformation connectedDevice, ConnectedDeviceEventArgs args) + { + if (args.EventId == ConnectedDeviceEventIds.ManufacturerEventId) + { + EdidManufacturerFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.NameEventId) + { + EdidNameFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.PreferredTimingEventId) + { + EdidPreferredTimingFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.SerialNumberEventId) + { + EdidSerialNumberFeedback.FireUpdate(); + } + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + /// + /// Gets the CEC stream directly from the HDMI port. + /// + public Cec StreamCec { get { return Rmc.HdmiOutput.StreamCec; } } + #endregion + + #region IRelayPorts Members + + public int NumberOfRelayPorts + { + get { return Rmc.NumberOfRelayPorts; } + } + + public CrestronCollection RelayPorts + { + get { return Rmc.RelayPorts; } + } + + #endregion + + #region IBasicVolumeWithFeedback Members + + public BoolFeedback MuteFeedback + { + get; + private set; + } + + /// + /// Not implemented + /// + public void MuteOff() + { + } + + /// + /// Not implemented + /// + public void MuteOn() + { + } + + public void SetVolume(ushort level) + { + Rmc.AudioOutput.Volume.UShortValue = level; + } + + public IntFeedback VolumeLevelFeedback + { + get; + private set; + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// Not implemented + /// + public void MuteToggle() + { + } + + public void VolumeDown(bool pressRelease) + { + if (pressRelease) + SigHelper.RampTimeScaled(Rmc.AudioOutput.Volume, 0, 4000); + else + Rmc.AudioOutput.Volume.StopRamp(); + } + + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + SigHelper.RampTimeScaled(Rmc.AudioOutput.Volume, 65535, 4000); + else + Rmc.AudioOutput.Volume.StopRamp(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4k100C1GController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4k100C1GController.cs new file mode 100644 index 00000000..571fa786 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4k100C1GController.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + + +namespace PepperDash.Essentials.DM +{ + public class DmRmc4k100C1GController : DmHdBaseTControllerBase, IRoutingInputsOutputs, + IIROutputPorts, IComPorts, ICec + { + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut }; } + } + + public DmRmc4k100C1GController(string key, string name, DmRmc4K100C1G rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + // Set Ports for CEC + HdmiOut.Port = Rmc; // Unique case, this class has no HdmiOutput port and ICec is implemented on the receiver class itself + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return (Rmc as DmRmc4K100C1G).IROutputPorts; } } + public int NumberOfIROutputPorts { get { return (Rmc as DmRmc4K100C1G).NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + public Cec StreamCec { get { return (Rmc as DmRmc4K100C1G).StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4kScalerCDspController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4kScalerCDspController.cs new file mode 100644 index 00000000..788b0e74 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmc4kScalerCDspController.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmRmc4kScalerCDspController : DmRmcControllerBase, IRoutingInputsOutputs, IBasicVolumeWithFeedback, + IIROutputPorts, IComPorts, ICec, IRelayPorts + { + public DmRmc4kScalerCDsp Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + public RoutingOutputPort BalancedAudioOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut, BalancedAudioOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmc4kScalerCDspController(string key, string name, DmRmc4kScalerCDsp rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + BalancedAudioOut = new RoutingOutputPort(DmPortName.BalancedAudioOut, eRoutingSignalType.Audio, + eRoutingPortConnectionType.LineAudio, null, this); + + MuteFeedback = new BoolFeedback(() => false); + VolumeLevelFeedback = new IntFeedback("MainVolumeLevelFeedback", () => + rmc.AudioOutput.VolumeFeedback.UShortValue); + + EdidManufacturerFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Manufacturer.StringValue); + EdidNameFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Name.StringValue); + EdidPreferredTimingFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.PreferredTiming.StringValue); + EdidSerialNumberFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.SerialNumber.StringValue); + + VideoOutputResolutionFeedback = new StringFeedback(() => Rmc.HdmiOutput.GetVideoResolutionString()); + + Rmc.HdmiOutput.OutputStreamChange += HdmiOutput_OutputStreamChange; + Rmc.HdmiOutput.ConnectedDevice.DeviceInformationChange += ConnectedDevice_DeviceInformationChange; + + // Set Ports for CEC + HdmiOut.Port = Rmc.HdmiOutput; + } + + void HdmiOutput_OutputStreamChange(EndpointOutputStream outputStream, EndpointOutputStreamEventArgs args) + { + if (args.EventId == EndpointOutputStreamEventIds.HorizontalResolutionFeedbackEventId || args.EventId == EndpointOutputStreamEventIds.VerticalResolutionFeedbackEventId || + args.EventId == EndpointOutputStreamEventIds.FramesPerSecondFeedbackEventId) + { + VideoOutputResolutionFeedback.FireUpdate(); + } + } + + void ConnectedDevice_DeviceInformationChange(ConnectedDeviceInformation connectedDevice, ConnectedDeviceEventArgs args) + { + if (args.EventId == ConnectedDeviceEventIds.ManufacturerEventId) + { + EdidManufacturerFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.NameEventId) + { + EdidNameFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.PreferredTimingEventId) + { + EdidPreferredTimingFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.SerialNumberEventId) + { + EdidSerialNumberFeedback.FireUpdate(); + } + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + /// + /// Gets the CEC stream directly from the HDMI port. + /// + public Cec StreamCec { get { return Rmc.HdmiOutput.StreamCec; } } + #endregion + + #region IRelayPorts Members + + public int NumberOfRelayPorts + { + get { return Rmc.NumberOfRelayPorts; } + } + + public CrestronCollection RelayPorts + { + get { return Rmc.RelayPorts; } + } + + #endregion + + #region IBasicVolumeWithFeedback Members + + public BoolFeedback MuteFeedback + { + get; + private set; + } + + /// + /// Not implemented + /// + public void MuteOff() + { + } + + /// + /// Not implemented + /// + public void MuteOn() + { + } + + public void SetVolume(ushort level) + { + Rmc.AudioOutput.Volume.UShortValue = level; + } + + public IntFeedback VolumeLevelFeedback + { + get; + private set; + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// Not implemented + /// + public void MuteToggle() + { + } + + public void VolumeDown(bool pressRelease) + { + if (pressRelease) + SigHelper.RampTimeScaled(Rmc.AudioOutput.Volume, 0, 4000); + else + Rmc.AudioOutput.Volume.StopRamp(); + } + + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + SigHelper.RampTimeScaled(Rmc.AudioOutput.Volume, 65535, 4000); + else + Rmc.AudioOutput.Volume.StopRamp(); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcHelper.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcHelper.cs new file mode 100644 index 00000000..16be9593 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcHelper.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM +{ + public abstract class DmRmcControllerBase : CrestronGenericBaseDevice + { + public virtual StringFeedback VideoOutputResolutionFeedback { get; protected set; } + public virtual StringFeedback EdidManufacturerFeedback { get; protected set; } + public virtual StringFeedback EdidNameFeedback { get; protected set; } + public virtual StringFeedback EdidPreferredTimingFeedback { get; protected set; } + public virtual StringFeedback EdidSerialNumberFeedback { get; protected set; } + + public DmRmcControllerBase(string key, string name, EndpointReceiverBase device) + : base(key, name, device) + { + // if wired to a chassis, skip registration step in base class + if (device.DMOutput != null) + { + this.PreventRegistration = true; + } + AddToFeedbackList(VideoOutputResolutionFeedback, EdidManufacturerFeedback, EdidSerialNumberFeedback, EdidNameFeedback, EdidPreferredTimingFeedback); + } + } + + public abstract class DmHdBaseTControllerBase : CrestronGenericBaseDevice + { + public HDBaseTBase Rmc { get; protected set; } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmHdBaseTControllerBase(string key, string name, HDBaseTBase rmc) + : base(key, name, rmc) + { + + } + } + + public class DmRmcHelper + { + /// + /// A factory method for various DmTxControllers + /// + /// + /// + /// + /// + public static CrestronGenericBaseDevice GetDmRmcController(string key, string name, string typeName, DmRmcPropertiesConfig props) + { + // switch on type name... later... + + typeName = typeName.ToLower(); + uint ipid = props.Control.IpIdInt; // Convert.ToUInt16(props.Id, 16); + + + + // right here, we need to grab the tie line that associates this + // RMC with a chassis or processor. If the RMC input's tie line is not + // connected to a chassis, then it's parent is the processor. + // If the RMC is connected to a chassis, then we need to grab the + // output number from the tie line and use that to plug it in. + // Example of chassis-connected: + //{ + // "sourceKey": "dmMd8x8-1", + // "sourcePort": "anyOut2", + // "destinationKey": "dmRmc100C-2", + // "destinationPort": "DmIn" + //} + + // Tx -> RMC link: + //{ + // "sourceKey": "dmTx201C-1", + // "sourcePort": "DmOut", + // "destinationKey": "dmRmc100C-2", + // "destinationPort": "DmIn" + //} + + var tlc = TieLineCollection.Default; + // grab the tie line that has this key as + // THIS DOESN'T WORK BECAUSE THE RMC THAT WE NEED (THIS) HASN'T BEEN MADE + // YET AND THUS WILL NOT HAVE A TIE LINE... + var inputTieLine = tlc.FirstOrDefault(t => + { + var d = t.DestinationPort.ParentDevice; + return d.Key.Equals(key, StringComparison.OrdinalIgnoreCase) + && d is DmChassisController; + }); + + var pKey = props.ParentDeviceKey.ToLower(); + + + + + // Non-DM-chassis endpoints + if (pKey == "processor") + { + // Catch constructor failures, mainly dues to IPID + try + { + if (typeName.StartsWith("dmrmc100c")) + return new DmRmcX100CController(key, name, new DmRmc100C(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmc4k100c")) + return new DmRmcX100CController(key, name, new DmRmc4k100C(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmc4kz100c")) + return new DmRmcX100CController(key, name, new DmRmc4kz100C(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmc150s")) + return new DmRmc150SController(key, name, new DmRmc150S(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmc200c")) + return new DmRmc200CController(key, name, new DmRmc200C(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmc200s")) + return new DmRmc200SController(key, name, new DmRmc200S(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmc200s2")) + return new DmRmc200S2Controller(key, name, new DmRmc200S2(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmcscalerc")) + return new DmRmcScalerCController(key, name, new DmRmcScalerC(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmcscalers")) + return new DmRmcScalerSController(key, name, new DmRmcScalerS(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmcscalers2")) + return new DmRmcScalerS2Controller(key, name, new DmRmcScalerS2(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmc4kscalerc")) + return new DmRmc4kScalerCController(key, name, new DmRmc4kScalerC(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmrmc4kscalercdsp")) + return new DmRmc4kScalerCDspController(key, name, new DmRmc4kScalerCDsp(ipid, Global.ControlSystem)); + } + catch (Exception e) + { + Debug.Console(0, "[{0}] WARNING: Cannot create DM-RMC device: {1}", key, e.Message); + } + + + Debug.Console(0, "Cannot create DM-RMC of type: '{0}'", typeName); + } + // Endpoints attached to DM Chassis + else + { + var parentDev = DeviceManager.GetDeviceForKey(pKey); + if (!(parentDev is DmChassisController)) + { + Debug.Console(0, "Cannot create DM device '{0}'. '{1}' is not a DM Chassis.", + key, pKey); + return null; + } + + var chassis = (parentDev as DmChassisController).Chassis; + var num = props.ParentOutputNumber; + if (num <= 0 || num > chassis.NumberOfOutputs) + { + Debug.Console(0, "Cannot create DM device '{0}'. Output number '{1}' is out of range", + key, num); + return null; + } + else + { + var controller = (parentDev as DmChassisController); + controller.RxDictionary.Add(num, key); + } + // Catch constructor failures, mainly dues to IPID + try + { + + // Must use different constructor for CPU3 chassis types. No IPID + if (chassis is DmMd8x8Cpu3 || chassis is DmMd16x16Cpu3 || + chassis is DmMd32x32Cpu3 || chassis is DmMd8x8Cpu3rps || + chassis is DmMd16x16Cpu3rps || chassis is DmMd32x32Cpu3rps) + { + if (typeName.StartsWith("hdbasetrx")) + return new HDBaseTRxController(key, name, new HDRx3CB(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc4k100c1g")) + return new DmRmc4k100C1GController(key, name, new DmRmc4K100C1G(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc100c")) + return new DmRmcX100CController(key, name, new DmRmc100C(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc4k100c")) + return new DmRmcX100CController(key, name, new DmRmc4k100C(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc4kz100c")) + return new DmRmcX100CController(key, name, new DmRmc4kz100C(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc150s")) + return new DmRmc150SController(key, name, new DmRmc150S(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc200c")) + return new DmRmc200CController(key, name, new DmRmc200C(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc200s")) + return new DmRmc200SController(key, name, new DmRmc200S(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc200s2")) + return new DmRmc200S2Controller(key, name, new DmRmc200S2(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmcscalerc")) + return new DmRmcScalerCController(key, name, new DmRmcScalerC(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmcscalers")) + return new DmRmcScalerSController(key, name, new DmRmcScalerS(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmcscalers2")) + return new DmRmcScalerS2Controller(key, name, new DmRmcScalerS2(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc4kscalerc")) + return new DmRmc4kScalerCController(key, name, new DmRmc4kScalerC(chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc4kscalercdsp")) + return new DmRmc4kScalerCDspController(key, name, new DmRmc4kScalerCDsp(chassis.Outputs[num])); + } + else + { + if (typeName.StartsWith("hdbasetrx")) + return new HDBaseTRxController(key, name, new HDRx3CB(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc4k100c1g")) + return new DmRmc4k100C1GController(key, name, new DmRmc4K100C1G(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc100c")) + return new DmRmcX100CController(key, name, new DmRmc100C(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc4k100c")) + return new DmRmcX100CController(key, name, new DmRmc4k100C(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc4kz100c")) + return new DmRmcX100CController(key, name, new DmRmc4kz100C(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc150s")) + return new DmRmc150SController(key, name, new DmRmc150S(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc200c")) + return new DmRmc200CController(key, name, new DmRmc200C(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc200s")) + return new DmRmc200SController(key, name, new DmRmc200S(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc200s2")) + return new DmRmc200S2Controller(key, name, new DmRmc200S2(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmcscalerc")) + return new DmRmcScalerCController(key, name, new DmRmcScalerC(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmcscalers")) + return new DmRmcScalerSController(key, name, new DmRmcScalerS(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmcscalers2")) + return new DmRmcScalerS2Controller(key, name, new DmRmcScalerS2(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc4kscalerc")) + return new DmRmc4kScalerCController(key, name, new DmRmc4kScalerC(ipid, chassis.Outputs[num])); + if (typeName.StartsWith("dmrmc4kscalercdsp")) + return new DmRmc4kScalerCDspController(key, name, new DmRmc4kScalerCDsp(ipid, chassis.Outputs[num])); + } + } + catch (Exception e) + { + Debug.Console(0, "[{0}] WARNING: Cannot create DM-RMC device: {1}", key, e.Message); + } + } + + return null; + } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerCController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerCController.cs new file mode 100644 index 00000000..a7c8407a --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerCController.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmRmcScalerCController : DmRmcControllerBase, IRoutingInputsOutputs, + IIROutputPorts, IComPorts, ICec + { + public DmRmcScalerC Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmcScalerCController(string key, string name, DmRmcScalerC rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + EdidManufacturerFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Manufacturer.StringValue); + EdidNameFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Name.StringValue); + EdidPreferredTimingFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.PreferredTiming.StringValue); + EdidSerialNumberFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.SerialNumber.StringValue); + + VideoOutputResolutionFeedback = new StringFeedback(() => Rmc.HdmiOutput.GetVideoResolutionString()); + + Rmc.HdmiOutput.OutputStreamChange += HdmiOutput_OutputStreamChange; + Rmc.HdmiOutput.ConnectedDevice.DeviceInformationChange += ConnectedDevice_DeviceInformationChange; + + // Set Ports for CEC + HdmiOut.Port = Rmc.HdmiOutput; + } + + void HdmiOutput_OutputStreamChange(EndpointOutputStream outputStream, EndpointOutputStreamEventArgs args) + { + if (args.EventId == EndpointOutputStreamEventIds.HorizontalResolutionFeedbackEventId || args.EventId == EndpointOutputStreamEventIds.VerticalResolutionFeedbackEventId || + args.EventId == EndpointOutputStreamEventIds.FramesPerSecondFeedbackEventId) + { + VideoOutputResolutionFeedback.FireUpdate(); + } + } + + void ConnectedDevice_DeviceInformationChange(ConnectedDeviceInformation connectedDevice, ConnectedDeviceEventArgs args) + { + if (args.EventId == ConnectedDeviceEventIds.ManufacturerEventId) + { + EdidManufacturerFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.NameEventId) + { + EdidNameFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.PreferredTimingEventId) + { + EdidPreferredTimingFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.SerialNumberEventId) + { + EdidSerialNumberFeedback.FireUpdate(); + } + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + /// + /// Gets the CEC stream directly from the HDMI port. + /// + public Cec StreamCec { get { return Rmc.HdmiOutput.StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerS2Controller.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerS2Controller.cs new file mode 100644 index 00000000..d4c574dd --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerS2Controller.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmRmcScalerS2Controller : DmRmcControllerBase, IRoutingInputsOutputs, + IIROutputPorts, IComPorts, ICec + { + public DmRmcScalerS2 Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmcScalerS2Controller(string key, string name, DmRmcScalerS2 rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + EdidManufacturerFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Manufacturer.StringValue); + EdidNameFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Name.StringValue); + EdidPreferredTimingFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.PreferredTiming.StringValue); + EdidSerialNumberFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.SerialNumber.StringValue); + + VideoOutputResolutionFeedback = new StringFeedback(() => Rmc.HdmiOutput.GetVideoResolutionString()); + + Rmc.HdmiOutput.OutputStreamChange += HdmiOutput_OutputStreamChange; + Rmc.HdmiOutput.ConnectedDevice.DeviceInformationChange += ConnectedDevice_DeviceInformationChange; + + // Set Ports for CEC + HdmiOut.Port = Rmc.HdmiOutput; + } + + void HdmiOutput_OutputStreamChange(EndpointOutputStream outputStream, EndpointOutputStreamEventArgs args) + { + if (args.EventId == EndpointOutputStreamEventIds.HorizontalResolutionFeedbackEventId || args.EventId == EndpointOutputStreamEventIds.VerticalResolutionFeedbackEventId || + args.EventId == EndpointOutputStreamEventIds.FramesPerSecondFeedbackEventId) + { + VideoOutputResolutionFeedback.FireUpdate(); + } + } + + void ConnectedDevice_DeviceInformationChange(ConnectedDeviceInformation connectedDevice, ConnectedDeviceEventArgs args) + { + if (args.EventId == ConnectedDeviceEventIds.ManufacturerEventId) + { + EdidManufacturerFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.NameEventId) + { + EdidNameFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.PreferredTimingEventId) + { + EdidPreferredTimingFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.SerialNumberEventId) + { + EdidSerialNumberFeedback.FireUpdate(); + } + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + /// + /// Gets the CEC stream directly from the HDMI port. + /// + public Cec StreamCec { get { return Rmc.HdmiOutput.StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerSController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerSController.cs new file mode 100644 index 00000000..12fdf554 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcScalerSController.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions + /// + /// + public class DmRmcScalerSController : DmRmcControllerBase, IRoutingInputsOutputs, + IIROutputPorts, IComPorts, ICec + { + public DmRmcScalerS Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmcScalerSController(string key, string name, DmRmcScalerS rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + EdidManufacturerFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Manufacturer.StringValue); + EdidNameFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.Name.StringValue); + EdidPreferredTimingFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.PreferredTiming.StringValue); + EdidSerialNumberFeedback = new StringFeedback(() => Rmc.HdmiOutput.ConnectedDevice.SerialNumber.StringValue); + + VideoOutputResolutionFeedback = new StringFeedback(() => Rmc.HdmiOutput.GetVideoResolutionString()); + + Rmc.HdmiOutput.OutputStreamChange += HdmiOutput_OutputStreamChange; + Rmc.HdmiOutput.ConnectedDevice.DeviceInformationChange += ConnectedDevice_DeviceInformationChange; + + // Set Ports for CEC + HdmiOut.Port = Rmc.HdmiOutput; + } + + void HdmiOutput_OutputStreamChange(EndpointOutputStream outputStream, EndpointOutputStreamEventArgs args) + { + if (args.EventId == EndpointOutputStreamEventIds.HorizontalResolutionFeedbackEventId || args.EventId == EndpointOutputStreamEventIds.VerticalResolutionFeedbackEventId || + args.EventId == EndpointOutputStreamEventIds.FramesPerSecondFeedbackEventId) + { + VideoOutputResolutionFeedback.FireUpdate(); + } + } + + void ConnectedDevice_DeviceInformationChange(ConnectedDeviceInformation connectedDevice, ConnectedDeviceEventArgs args) + { + if (args.EventId == ConnectedDeviceEventIds.ManufacturerEventId) + { + EdidManufacturerFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.NameEventId) + { + EdidNameFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.PreferredTimingEventId) + { + EdidPreferredTimingFeedback.FireUpdate(); + } + else if (args.EventId == ConnectedDeviceEventIds.SerialNumberEventId) + { + EdidSerialNumberFeedback.FireUpdate(); + } + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + /// + /// Gets the CEC stream directly from the HDMI port. + /// + public Cec StreamCec { get { return Rmc.HdmiOutput.StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcX100CController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcX100CController.cs new file mode 100644 index 00000000..4b21ce3e --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Receivers/DmRmcX100CController.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// Builds a controller for basic DM-RMCs (both 4K and non-4K) with Com and IR ports and no control functions + /// + /// + public class DmRmcX100CController : DmRmcControllerBase, IRoutingInputsOutputs, + IIROutputPorts, IComPorts, ICec + { + public DmRmc100C Rmc { get; private set; } + + public RoutingInputPort DmIn { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { DmIn }; } + } + + public RoutingPortCollection OutputPorts + { + get { return new RoutingPortCollection { HdmiOut }; } + } + + /// + /// Make a Crestron RMC and put it in here + /// + public DmRmcX100CController(string key, string name, DmRmc100C rmc) + : base(key, name, rmc) + { + Rmc = rmc; + DmIn = new RoutingInputPort(DmPortName.DmIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, 0, this); + HdmiOut = new RoutingOutputPort(DmPortName.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + // Set Ports for CEC + HdmiOut.Port = Rmc; // Unique case, this class has no HdmiOutput port and ICec is implemented on the receiver class itself + } + + public override bool CustomActivate() + { + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Rmc.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Rmc.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Rmc.ComPorts; } } + public int NumberOfComPorts { get { return Rmc.NumberOfComPorts; } } + #endregion + + #region ICec Members + public Cec StreamCec { get { return Rmc.StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx200Controller.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx200Controller.cs new file mode 100644 index 00000000..11b2f1b4 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx200Controller.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM +{ + // using eVst = Crestron.SimplSharpPro.DeviceSupport.eX02VideoSourceType; + + /// + /// Controller class for all DM-TX-201C/S/F transmitters + /// + public class DmTx200Controller : DmTxControllerBase, ITxRouting, IHasFeedback + { + public DmTx200C2G Tx { get; private set; } + + public RoutingInputPortWithVideoStatuses HdmiInput { get; private set; } + public RoutingInputPortWithVideoStatuses VgaInput { get; private set; } + public RoutingOutputPort DmOutput { get; private set; } + + public override StringFeedback ActiveVideoInputFeedback { get; protected set; } + public IntFeedback VideoSourceNumericFeedback { get; protected set; } + public IntFeedback AudioSourceNumericFeedback { get; protected set; } + public IntFeedback HdmiInHdcpCapabilityFeedback { get; protected set; } + + //public override IntFeedback HdcpSupportAllFeedback { get; protected set; } + //public override ushort HdcpSupportCapability { get; protected set; } + + /// + /// Helps get the "real" inputs, including when in Auto + /// + public DmTx200Base.eSourceSelection ActualActiveVideoInput + { + get + { + if (Tx.VideoSourceFeedback == DmTx200Base.eSourceSelection.Digital || + Tx.VideoSourceFeedback == DmTx200Base.eSourceSelection.Analog || + Tx.VideoSourceFeedback == DmTx200Base.eSourceSelection.Disable) + return Tx.VideoSourceFeedback; + else // auto + { + if (Tx.HdmiInput.SyncDetectedFeedback.BoolValue) + return DmTx200Base.eSourceSelection.Digital; + else if (Tx.VgaInput.SyncDetectedFeedback.BoolValue) + return DmTx200Base.eSourceSelection.Analog; + else + return DmTx200Base.eSourceSelection.Disable; + } + } + } + + public RoutingPortCollection InputPorts + { + get + { + return new RoutingPortCollection + { + HdmiInput, + VgaInput, + AnyVideoInput + }; + } + } + + public RoutingPortCollection OutputPorts + { + get + { + return new RoutingPortCollection { DmOutput }; + } + } + + /// + /// + /// + /// + /// + /// + public DmTx200Controller(string key, string name, DmTx200C2G tx) + : base(key, name, tx) + { + Tx = tx; + + HdmiInput = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, DmTx200Base.eSourceSelection.Digital, this, + VideoStatusHelper.GetHdmiInputStatusFuncs(tx.HdmiInput)); + VgaInput = new RoutingInputPortWithVideoStatuses(DmPortName.VgaIn, + eRoutingSignalType.Video, eRoutingPortConnectionType.Vga, DmTx200Base.eSourceSelection.Analog, this, + VideoStatusHelper.GetVgaInputStatusFuncs(tx.VgaInput)); + + ActiveVideoInputFeedback = new StringFeedback("ActiveVideoInput", + () => ActualActiveVideoInput.ToString()); + + Tx.HdmiInput.InputStreamChange += new EndpointInputStreamChangeEventHandler(InputStreamChangeEvent); + Tx.BaseEvent += Tx_BaseEvent; + Tx.OnlineStatusChange += new OnlineStatusChangeEventHandler(Tx_OnlineStatusChange); + + VideoSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.VideoSourceFeedback; + }); + AudioSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.AudioSourceFeedback; + }); + + HdmiInHdcpCapabilityFeedback = new IntFeedback("HdmiInHdcpCapability", () => + { + if (tx.HdmiInput.HdpcSupportOnFeedback.BoolValue) + return 1; + else + return 0; + }); + + HdcpSupportCapability = eHdcpCapabilityType.HdcpAutoSupport; + + var combinedFuncs = new VideoStatusFuncsWrapper + { + HdcpActiveFeedbackFunc = () => + (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Digital + && tx.HdmiInput.VideoAttributes.HdcpActiveFeedback.BoolValue), + + HdcpStateFeedbackFunc = () => + { + if (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Digital) + return tx.HdmiInput.VideoAttributes.HdcpStateFeedback.ToString(); + return ""; + }, + + VideoResolutionFeedbackFunc = () => + { + if (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Digital) + return tx.HdmiInput.VideoAttributes.GetVideoResolutionString(); + if (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Analog) + return tx.VgaInput.VideoAttributes.GetVideoResolutionString(); + return ""; + }, + VideoSyncFeedbackFunc = () => + (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Digital + && tx.HdmiInput.SyncDetectedFeedback.BoolValue) + || (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Analog + && tx.VgaInput.SyncDetectedFeedback.BoolValue) + || (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Auto + && (tx.VgaInput.SyncDetectedFeedback.BoolValue || tx.HdmiInput.SyncDetectedFeedback.BoolValue)) + + }; + + AnyVideoInput = new RoutingInputPortWithVideoStatuses(DmPortName.AnyVideoIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.None, 0, this, combinedFuncs); + + DmOutput = new RoutingOutputPort(DmPortName.DmOut, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmCat, null, this); + + AddToFeedbackList(ActiveVideoInputFeedback, VideoSourceNumericFeedback, AudioSourceNumericFeedback, + AnyVideoInput.VideoStatus.HasVideoStatusFeedback, AnyVideoInput.VideoStatus.HdcpActiveFeedback, + AnyVideoInput.VideoStatus.HdcpStateFeedback, AnyVideoInput.VideoStatus.VideoResolutionFeedback, + AnyVideoInput.VideoStatus.VideoSyncFeedback, HdmiInHdcpCapabilityFeedback); + + // Set Ports for CEC + HdmiInput.Port = Tx.HdmiInput; + VgaInput.Port = Tx.VgaInput; + DmOutput.Port = Tx.DmOutput; + } + + void Tx_OnlineStatusChange(GenericBase currentDevice, OnlineOfflineEventArgs args) + { + ActiveVideoInputFeedback.FireUpdate(); + VideoSourceNumericFeedback.FireUpdate(); + AudioSourceNumericFeedback.FireUpdate(); + + } + + public override bool CustomActivate() + { + + Tx.HdmiInput.InputStreamChange += (o, a) => FowardInputStreamChange(HdmiInput, a.EventId); + Tx.HdmiInput.VideoAttributes.AttributeChange += (o, a) => FireVideoAttributeChange(HdmiInput, a.EventId); + + Tx.VgaInput.InputStreamChange += (o, a) => FowardInputStreamChange(VgaInput, a.EventId); + Tx.VgaInput.VideoAttributes.AttributeChange += (o, a) => FireVideoAttributeChange(VgaInput, a.EventId); + + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + public void ExecuteNumericSwitch(ushort input, ushort output, eRoutingSignalType type) + { + Debug.Console(2, this, "Executing Numeric Switch to input {0}.", input); + + switch (input) + { + case 0: + { + ExecuteSwitch(DmTx200Base.eSourceSelection.Auto, null, type); + break; + } + case 1: + { + ExecuteSwitch(HdmiInput.Selector, null, type); + break; + } + case 2: + { + ExecuteSwitch(VgaInput.Selector, null, type); + break; + } + case 3: + { + ExecuteSwitch(DmTx200Base.eSourceSelection.Disable, null, type); + break; + } + } + } + + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) + { + if ((signalType | eRoutingSignalType.Video) == eRoutingSignalType.Video) + Tx.VideoSource = (DmTx200Base.eSourceSelection)inputSelector; + if ((signalType | eRoutingSignalType.Audio) == eRoutingSignalType.Audio) + Tx.AudioSource = (DmTx200Base.eSourceSelection)inputSelector; + } + + void Tx_BaseEvent(GenericBase device, BaseEventArgs args) + { + var id = args.EventId; + Debug.Console(2, this, "EventId {0}", args.EventId); + + if (id == EndpointTransmitterBase.VideoSourceFeedbackEventId) + { + Debug.Console(2, this, " Video Source: {0}", Tx.VideoSourceFeedback); + VideoSourceNumericFeedback.FireUpdate(); + ActiveVideoInputFeedback.FireUpdate(); + } + + // ------------------------------ incomplete ----------------------------------------- + else if (id == EndpointTransmitterBase.AudioSourceFeedbackEventId) + { + Debug.Console(2, this, " Audio Source: {0}", Tx.AudioSourceFeedback); + AudioSourceNumericFeedback.FireUpdate(); + } + } + + void InputStreamChangeEvent(EndpointInputStream inputStream, EndpointInputStreamEventArgs args) + { + Debug.Console(2, "{0} event {1} stream {2}", this.Tx.ToString(), inputStream.ToString(), args.EventId.ToString()); + + if (args.EventId == EndpointInputStreamEventIds.HdcpSupportOffFeedbackEventId) + { + HdmiInHdcpCapabilityFeedback.FireUpdate(); + } + else if (args.EventId == EndpointInputStreamEventIds.HdcpSupportOnFeedbackEventId) + { + HdmiInHdcpCapabilityFeedback.FireUpdate(); + } + } + + /// + /// Relays the input stream change to the appropriate RoutingInputPort. + /// + void FowardInputStreamChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + if (eventId == EndpointInputStreamEventIds.SyncDetectedFeedbackEventId) + { + inputPort.VideoStatus.VideoSyncFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoSyncFeedback.FireUpdate(); + } + } + + /// + /// Relays the VideoAttributes change to a RoutingInputPort + /// + void FireVideoAttributeChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + //// LOCATION: Crestron.SimplSharpPro.DM.VideoAttributeEventIds + //Debug.Console(2, this, "VideoAttributes_AttributeChange event id={0} from {1}", + // args.EventId, (sender as VideoAttributesEnhanced).Owner.GetType()); + switch (eventId) + { + case VideoAttributeEventIds.HdcpActiveFeedbackEventId: + inputPort.VideoStatus.HdcpActiveFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpActiveFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HdcpStateFeedbackEventId: + inputPort.VideoStatus.HdcpStateFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpStateFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HorizontalResolutionFeedbackEventId: + case VideoAttributeEventIds.VerticalResolutionFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.FramesPerSecondFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + } + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx201CController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx201CController.cs new file mode 100644 index 00000000..fb10829c --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx201CController.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM +{ + // using eVst = Crestron.SimplSharpPro.DeviceSupport.eX02VideoSourceType; + + /// + /// Controller class for all DM-TX-201C/S/F transmitters + /// + public class DmTx201XController : DmTxControllerBase, ITxRouting, IHasFeedback + { + public DmTx201S Tx { get; private set; } // uses the 201S class as it is the base class for the 201C + + public RoutingInputPortWithVideoStatuses HdmiInput { get; private set; } + public RoutingInputPortWithVideoStatuses VgaInput { get; private set; } + public RoutingOutputPort DmOutput { get; private set; } + public RoutingOutputPort HdmiLoopOut { get; private set; } + + public override StringFeedback ActiveVideoInputFeedback { get; protected set; } + public IntFeedback VideoSourceNumericFeedback { get; protected set; } + public IntFeedback AudioSourceNumericFeedback { get; protected set; } + public IntFeedback HdmiInHdcpCapabilityFeedback { get; protected set; } + + //public override IntFeedback HdcpSupportAllFeedback { get; protected set; } + //public override ushort HdcpSupportCapability { get; protected set; } + + /// + /// Helps get the "real" inputs, including when in Auto + /// + public DmTx200Base.eSourceSelection ActualActiveVideoInput + { + get + { + if (Tx.VideoSourceFeedback == DmTx200Base.eSourceSelection.Digital || + Tx.VideoSourceFeedback == DmTx200Base.eSourceSelection.Analog || + Tx.VideoSourceFeedback == DmTx200Base.eSourceSelection.Disable) + return Tx.VideoSourceFeedback; + else // auto + { + if (Tx.HdmiInput.SyncDetectedFeedback.BoolValue) + return DmTx200Base.eSourceSelection.Digital; + else if (Tx.VgaInput.SyncDetectedFeedback.BoolValue) + return DmTx200Base.eSourceSelection.Analog; + else + return DmTx200Base.eSourceSelection.Disable; + } + } + } + + public RoutingPortCollection InputPorts + { + get + { + return new RoutingPortCollection + { + HdmiInput, + VgaInput, + AnyVideoInput + }; + } + } + + public RoutingPortCollection OutputPorts + { + get + { + return new RoutingPortCollection { DmOutput, HdmiLoopOut }; + } + } + + /// + /// + /// + /// + /// + /// + public DmTx201XController(string key, string name, DmTx201S tx) + : base(key, name, tx) + { + Tx = tx; + + HdmiInput = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, DmTx200Base.eSourceSelection.Digital, this, + VideoStatusHelper.GetHdmiInputStatusFuncs(tx.HdmiInput)); + VgaInput = new RoutingInputPortWithVideoStatuses(DmPortName.VgaIn, + eRoutingSignalType.Video, eRoutingPortConnectionType.Vga, DmTx200Base.eSourceSelection.Analog, this, + VideoStatusHelper.GetVgaInputStatusFuncs(tx.VgaInput)); + + ActiveVideoInputFeedback = new StringFeedback("ActiveVideoInput", + () => ActualActiveVideoInput.ToString()); + + Tx.HdmiInput.InputStreamChange += new EndpointInputStreamChangeEventHandler(InputStreamChangeEvent); + Tx.BaseEvent += Tx_BaseEvent; + Tx.OnlineStatusChange += new OnlineStatusChangeEventHandler(Tx_OnlineStatusChange); + + VideoSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.VideoSourceFeedback; + }); + AudioSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.AudioSourceFeedback; + }); + + HdmiInHdcpCapabilityFeedback = new IntFeedback("HdmiInHdcpCapability", () => + { + if (tx.HdmiInput.HdpcSupportOnFeedback.BoolValue) + return 1; + else + return 0; + }); + + HdcpSupportCapability = eHdcpCapabilityType.HdcpAutoSupport; + + var combinedFuncs = new VideoStatusFuncsWrapper + { + HdcpActiveFeedbackFunc = () => + (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Digital + && tx.HdmiInput.VideoAttributes.HdcpActiveFeedback.BoolValue), + + HdcpStateFeedbackFunc = () => + { + if (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Digital) + return tx.HdmiInput.VideoAttributes.HdcpStateFeedback.ToString(); + return ""; + }, + + VideoResolutionFeedbackFunc = () => + { + if (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Digital) + return tx.HdmiInput.VideoAttributes.GetVideoResolutionString(); + if (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Analog) + return tx.VgaInput.VideoAttributes.GetVideoResolutionString(); + return ""; + }, + VideoSyncFeedbackFunc = () => + (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Digital + && tx.HdmiInput.SyncDetectedFeedback.BoolValue) + || (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Analog + && tx.VgaInput.SyncDetectedFeedback.BoolValue) + || (ActualActiveVideoInput == DmTx200Base.eSourceSelection.Auto + && (tx.VgaInput.SyncDetectedFeedback.BoolValue || tx.HdmiInput.SyncDetectedFeedback.BoolValue)) + + }; + + AnyVideoInput = new RoutingInputPortWithVideoStatuses(DmPortName.AnyVideoIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.None, 0, this, combinedFuncs); + + DmOutput = new RoutingOutputPort(DmPortName.DmOut, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmCat, null, this); + HdmiLoopOut = new RoutingOutputPort(DmPortName.HdmiLoopOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + AddToFeedbackList(ActiveVideoInputFeedback, VideoSourceNumericFeedback, AudioSourceNumericFeedback, + AnyVideoInput.VideoStatus.HasVideoStatusFeedback, AnyVideoInput.VideoStatus.HdcpActiveFeedback, + AnyVideoInput.VideoStatus.HdcpStateFeedback, AnyVideoInput.VideoStatus.VideoResolutionFeedback, + AnyVideoInput.VideoStatus.VideoSyncFeedback, HdmiInHdcpCapabilityFeedback); + + // Set Ports for CEC + HdmiInput.Port = Tx.HdmiInput; + VgaInput.Port = Tx.VgaInput; + HdmiLoopOut.Port = Tx.HdmiOutput; + DmOutput.Port = Tx.DmOutput; + } + + void Tx_OnlineStatusChange(GenericBase currentDevice, OnlineOfflineEventArgs args) + { + ActiveVideoInputFeedback.FireUpdate(); + VideoSourceNumericFeedback.FireUpdate(); + AudioSourceNumericFeedback.FireUpdate(); + + } + + public override bool CustomActivate() + { + + Tx.HdmiInput.InputStreamChange += (o, a) => FowardInputStreamChange(HdmiInput, a.EventId); + Tx.HdmiInput.VideoAttributes.AttributeChange += (o, a) => FireVideoAttributeChange(HdmiInput, a.EventId); + + Tx.VgaInput.InputStreamChange += (o, a) => FowardInputStreamChange(VgaInput, a.EventId); + Tx.VgaInput.VideoAttributes.AttributeChange += (o, a) => FireVideoAttributeChange(VgaInput, a.EventId); + + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + public void ExecuteNumericSwitch(ushort input, ushort output, eRoutingSignalType type) + { + Debug.Console(2, this, "Executing Numeric Switch to input {0}.", input); + + switch (input) + { + case 0: + { + ExecuteSwitch(DmTx200Base.eSourceSelection.Auto, null, type); + break; + } + case 1: + { + ExecuteSwitch(HdmiInput.Selector, null, type); + break; + } + case 2: + { + ExecuteSwitch(VgaInput.Selector, null, type); + break; + } + case 3: + { + ExecuteSwitch(DmTx200Base.eSourceSelection.Disable, null, type); + break; + } + } + } + + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) + { + if((signalType | eRoutingSignalType.Video) == eRoutingSignalType.Video) + Tx.VideoSource = (DmTx200Base.eSourceSelection)inputSelector; + if ((signalType | eRoutingSignalType.Audio) == eRoutingSignalType.Audio) + Tx.AudioSource = (DmTx200Base.eSourceSelection)inputSelector; + } + + void Tx_BaseEvent(GenericBase device, BaseEventArgs args) + { + var id = args.EventId; + Debug.Console(2, this, "EventId {0}", args.EventId); + + if (id == EndpointTransmitterBase.VideoSourceFeedbackEventId) + { + Debug.Console(2, this, " Video Source: {0}", Tx.VideoSourceFeedback); + VideoSourceNumericFeedback.FireUpdate(); + ActiveVideoInputFeedback.FireUpdate(); + } + + // ------------------------------ incomplete ----------------------------------------- + else if (id == EndpointTransmitterBase.AudioSourceFeedbackEventId) + { + Debug.Console(2, this, " Audio Source: {0}", Tx.AudioSourceFeedback); + AudioSourceNumericFeedback.FireUpdate(); + } + } + + void InputStreamChangeEvent(EndpointInputStream inputStream, EndpointInputStreamEventArgs args) + { + Debug.Console(2, "{0} event {1} stream {2}", this.Tx.ToString(), inputStream.ToString(), args.EventId.ToString()); + + if (args.EventId == EndpointInputStreamEventIds.HdcpSupportOffFeedbackEventId) + { + HdmiInHdcpCapabilityFeedback.FireUpdate(); + } + else if (args.EventId == EndpointInputStreamEventIds.HdcpSupportOnFeedbackEventId) + { + HdmiInHdcpCapabilityFeedback.FireUpdate(); + } + } + + /// + /// Relays the input stream change to the appropriate RoutingInputPort. + /// + void FowardInputStreamChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + if (eventId == EndpointInputStreamEventIds.SyncDetectedFeedbackEventId) + { + inputPort.VideoStatus.VideoSyncFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoSyncFeedback.FireUpdate(); + } + } + + /// + /// Relays the VideoAttributes change to a RoutingInputPort + /// + void FireVideoAttributeChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + //// LOCATION: Crestron.SimplSharpPro.DM.VideoAttributeEventIds + //Debug.Console(2, this, "VideoAttributes_AttributeChange event id={0} from {1}", + // args.EventId, (sender as VideoAttributesEnhanced).Owner.GetType()); + switch (eventId) + { + case VideoAttributeEventIds.HdcpActiveFeedbackEventId: + inputPort.VideoStatus.HdcpActiveFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpActiveFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HdcpStateFeedbackEventId: + inputPort.VideoStatus.HdcpStateFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpStateFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HorizontalResolutionFeedbackEventId: + case VideoAttributeEventIds.VerticalResolutionFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.FramesPerSecondFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + } + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx401CController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx401CController.cs new file mode 100644 index 00000000..eb5c10fc --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx401CController.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM +{ + using eVst = DmTx401C.eSourceSelection; + + public class DmTx401CController : DmTxControllerBase, ITxRouting, IHasFeedback, IIROutputPorts, IComPorts + { + public DmTx401C Tx { get; private set; } + + public RoutingInputPortWithVideoStatuses HdmiIn { get; private set; } + public RoutingInputPortWithVideoStatuses DisplayPortIn { get; private set; } + public RoutingInputPortWithVideoStatuses VgaIn { get; private set; } + public RoutingInputPortWithVideoStatuses CompositeIn { get; private set; } + public RoutingOutputPort DmOut { get; private set; } + + public override StringFeedback ActiveVideoInputFeedback { get; protected set; } + public IntFeedback VideoSourceNumericFeedback { get; protected set; } + public IntFeedback AudioSourceNumericFeedback { get; protected set; } + public IntFeedback HdmiInHdcpCapabilityFeedback { get; protected set; } + + /// + /// Helps get the "real" inputs, including when in Auto + /// + public BaseDmTx401.eSourceSelection ActualVideoInput + { + get + { + if (Tx.VideoSourceFeedback != BaseDmTx401.eSourceSelection.Auto) + return Tx.VideoSourceFeedback; + else // auto + { + if (Tx.HdmiInput.SyncDetectedFeedback.BoolValue) + return BaseDmTx401.eSourceSelection.HDMI; + else if (Tx.VgaInput.SyncDetectedFeedback.BoolValue) + return BaseDmTx401.eSourceSelection.VGA; + else if (Tx.DisplayPortInput.SyncDetectedFeedback.BoolValue) + return BaseDmTx401.eSourceSelection.DisplayPort; + else if (Tx.CvbsInput.SyncDetectedFeedback.BoolValue) + return BaseDmTx401.eSourceSelection.Composite; + else + return BaseDmTx401.eSourceSelection.Disabled; + } + } + } + + public RoutingPortCollection InputPorts + { + get + { + return new RoutingPortCollection + { + HdmiIn, + DisplayPortIn, + VgaIn, + CompositeIn, + AnyVideoInput + }; + } + } + + public RoutingPortCollection OutputPorts + { + get + { + return new RoutingPortCollection { DmOut }; + } + } + + /// + /// + /// + /// + /// + /// + public DmTx401CController(string key, string name, DmTx401C tx) + : base(key, name, tx) + { + Tx = tx; + + HdmiIn = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, eVst.HDMI, this, + VideoStatusHelper.GetHdmiInputStatusFuncs(tx.HdmiInput)); + DisplayPortIn = new RoutingInputPortWithVideoStatuses(DmPortName.DisplayPortIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, eVst.DisplayPort, this, + VideoStatusHelper.GetDisplayPortInputStatusFuncs(tx.DisplayPortInput)); + VgaIn = new RoutingInputPortWithVideoStatuses(DmPortName.VgaIn, + eRoutingSignalType.Video, eRoutingPortConnectionType.Vga, eVst.VGA, this, + VideoStatusHelper.GetVgaInputStatusFuncs(tx.VgaInput)); + CompositeIn = new RoutingInputPortWithVideoStatuses(DmPortName.CompositeIn, + eRoutingSignalType.Video, eRoutingPortConnectionType.Composite, eVst.Composite, this, + VideoStatusHelper.GetVgaInputStatusFuncs(tx.VgaInput)); + + Tx.BaseEvent += Tx_BaseEvent; + + ActiveVideoInputFeedback = new StringFeedback("ActiveVideoInput", + () => ActualVideoInput.ToString()); + + VideoSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.VideoSourceFeedback; + }); + AudioSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.AudioSourceFeedback; + }); + + HdmiInHdcpCapabilityFeedback = new IntFeedback("HdmiInHdcpCapability", () => + { + if (tx.HdmiInput.HdpcSupportOnFeedback.BoolValue) + return 1; + else + return 0; + }); + + HdcpSupportCapability = eHdcpCapabilityType.HdcpAutoSupport; + + var combinedFuncs = new VideoStatusFuncsWrapper + { + HdcpActiveFeedbackFunc = () => + (ActualVideoInput == eVst.HDMI + && tx.HdmiInput.VideoAttributes.HdcpActiveFeedback.BoolValue) + || (ActualVideoInput == eVst.DisplayPort + && tx.DisplayPortInput.VideoAttributes.HdcpActiveFeedback.BoolValue), + + HdcpStateFeedbackFunc = () => + { + if (ActualVideoInput == eVst.HDMI) + return tx.HdmiInput.VideoAttributes.HdcpStateFeedback.ToString(); + if (ActualVideoInput == eVst.DisplayPort) + return tx.DisplayPortInput.VideoAttributes.HdcpStateFeedback.ToString(); + return ""; + }, + + VideoResolutionFeedbackFunc = () => + { + if (ActualVideoInput == eVst.HDMI) + return tx.HdmiInput.VideoAttributes.GetVideoResolutionString(); + if (ActualVideoInput == eVst.DisplayPort) + return tx.DisplayPortInput.VideoAttributes.GetVideoResolutionString(); + if (ActualVideoInput == eVst.VGA) + return tx.VgaInput.VideoAttributes.GetVideoResolutionString(); + if (ActualVideoInput == eVst.Composite) + return tx.CvbsInput.VideoAttributes.GetVideoResolutionString(); + return ""; + }, + VideoSyncFeedbackFunc = () => + (ActualVideoInput == eVst.HDMI + && tx.HdmiInput.SyncDetectedFeedback.BoolValue) + || (ActualVideoInput == eVst.DisplayPort + && tx.DisplayPortInput.SyncDetectedFeedback.BoolValue) + || (ActualVideoInput == eVst.VGA + && tx.VgaInput.SyncDetectedFeedback.BoolValue) + || (ActualVideoInput == eVst.Composite + && tx.CvbsInput.SyncDetectedFeedback.BoolValue) + }; + + AnyVideoInput = new RoutingInputPortWithVideoStatuses(DmPortName.AnyVideoIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.None, 0, this, combinedFuncs); + + DmOut = new RoutingOutputPort(DmPortName.DmOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, null, this); + + AddToFeedbackList(ActiveVideoInputFeedback, VideoSourceNumericFeedback, AudioSourceNumericFeedback, + AnyVideoInput.VideoStatus.HasVideoStatusFeedback, AnyVideoInput.VideoStatus.HdcpActiveFeedback, + AnyVideoInput.VideoStatus.HdcpStateFeedback, AnyVideoInput.VideoStatus.VideoResolutionFeedback, + AnyVideoInput.VideoStatus.VideoSyncFeedback, HdmiInHdcpCapabilityFeedback); + + // Set Ports for CEC + DisplayPortIn.Port = Tx.DisplayPortInput; + HdmiIn.Port = Tx.HdmiInput; + DmOut.Port = Tx.DmOutput; + } + + public override bool CustomActivate() + { + // Link up all of these damned events to the various RoutingPorts via a helper handler + Tx.HdmiInput.InputStreamChange += (o, a) => FowardInputStreamChange(HdmiIn, a.EventId); + Tx.HdmiInput.VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(HdmiIn, a.EventId); + + Tx.DisplayPortInput.InputStreamChange += (o, a) => FowardInputStreamChange(DisplayPortIn, a.EventId); + Tx.DisplayPortInput.VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(DisplayPortIn, a.EventId); + + Tx.VgaInput.InputStreamChange += (o, a) => FowardInputStreamChange(VgaIn, a.EventId); + Tx.VgaInput.VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(VgaIn, a.EventId); + + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + public void ExecuteNumericSwitch(ushort input, ushort output, eRoutingSignalType type) + { + Debug.Console(2, this, "Executing Numeric Switch to input {0}.", input); + + switch (input) + { + case 0: + { + ExecuteSwitch(eVst.Auto, null, type); + break; + } + case 1: + { + ExecuteSwitch(DisplayPortIn.Selector, null, type); + break; + } + case 2: + { + ExecuteSwitch(HdmiIn.Selector, null, type); + break; + } + case 3: + { + ExecuteSwitch(VgaIn.Selector, null, type); + break; + } + case 4: + { + ExecuteSwitch(CompositeIn.Selector, null, type); + break; + } + case 5: + { + ExecuteSwitch(eVst.Disabled, null, type); + break; + } + } + + } + + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) + { + if ((signalType | eRoutingSignalType.Video) == eRoutingSignalType.Video) + Tx.VideoSource = (eVst)inputSelector; + if ((signalType | eRoutingSignalType.Audio) == eRoutingSignalType.Audio) + Tx.AudioSource = (eVst)inputSelector; + } + + void Tx_BaseEvent(GenericBase device, BaseEventArgs args) + { + var id = args.EventId; + if (id == EndpointTransmitterBase.VideoSourceFeedbackEventId) + { + Debug.Console(2, this, " Video Source: {0}", Tx.VideoSourceFeedback); + VideoSourceNumericFeedback.FireUpdate(); + ActiveVideoInputFeedback.FireUpdate(); + } + + // ------------------------------ incomplete ----------------------------------------- + else if (id == EndpointTransmitterBase.AudioSourceFeedbackEventId) + { + Debug.Console(2, this, " Audio Source: {0}", Tx.AudioSourceFeedback); + AudioSourceNumericFeedback.FireUpdate(); + } + } + + /// + /// Relays the input stream change to the appropriate RoutingInputPort. + /// + void FowardInputStreamChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + if (eventId == EndpointInputStreamEventIds.SyncDetectedFeedbackEventId) + { + inputPort.VideoStatus.VideoSyncFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoSyncFeedback.FireUpdate(); + } + } + + /// + /// Relays the VideoAttributes change to a RoutingInputPort + /// + void ForwardVideoAttributeChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + //// LOCATION: Crestron.SimplSharpPro.DM.VideoAttributeEventIds + //Debug.Console(2, this, "VideoAttributes_AttributeChange event id={0} from {1}", + // args.EventId, (sender as VideoAttributesEnhanced).Owner.GetType()); + switch (eventId) + { + case VideoAttributeEventIds.HdcpActiveFeedbackEventId: + inputPort.VideoStatus.HdcpActiveFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpActiveFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HdcpStateFeedbackEventId: + inputPort.VideoStatus.HdcpStateFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpStateFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HorizontalResolutionFeedbackEventId: + case VideoAttributeEventIds.VerticalResolutionFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.FramesPerSecondFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + } + } + + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Tx.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Tx.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Tx.ComPorts; } } + public int NumberOfComPorts { get { return Tx.NumberOfComPorts; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k100Controller.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k100Controller.cs new file mode 100644 index 00000000..5d3c7354 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k100Controller.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM +{ + using eVst = Crestron.SimplSharpPro.DeviceSupport.eX02VideoSourceType; + using eAst = Crestron.SimplSharpPro.DeviceSupport.eX02AudioSourceType; + + public class DmTx4k100Controller : BasicDmTxControllerBase, IRoutingInputsOutputs, IHasFeedback, + IIROutputPorts, IComPorts, ICec + { + public DmTx4K100C1G Tx { get; private set; } + + public RoutingInputPort HdmiIn { get; private set; } + public RoutingOutputPort DmOut { get; private set; } + + //public IntFeedback VideoSourceNumericFeedback { get; protected set; } + //public IntFeedback AudioSourceNumericFeedback { get; protected set; } + //public IntFeedback HdmiIn1HdcpCapabilityFeedback { get; protected set; } + //public IntFeedback HdmiIn2HdcpCapabilityFeedback { get; protected set; } + + //public override IntFeedback HdcpSupportAllFeedback { get; protected set; } + //public override ushort HdcpSupportCapability { get; protected set; } + + /// + /// Helps get the "real" inputs, including when in Auto + /// + public Crestron.SimplSharpPro.DeviceSupport.eX02VideoSourceType ActualActiveVideoInput + { + get + { + return eVst.Hdmi1; + } + } + public RoutingPortCollection InputPorts + { + get + { + return new RoutingPortCollection + { + HdmiIn + }; + } + } + public RoutingPortCollection OutputPorts + { + get + { + return new RoutingPortCollection { DmOut }; + } + } + public DmTx4k100Controller(string key, string name, DmTx4K100C1G tx) + : base(key, name, tx) + { + Tx = tx; + + HdmiIn = new RoutingInputPort(DmPortName.HdmiIn1, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, eVst.Hdmi1, this); + + DmOut = new RoutingOutputPort(DmPortName.DmOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, null, this); + + // Set Ports for CEC + HdmiIn.Port = Tx; + } + + + + public override bool CustomActivate() + { + // Link up all of these damned events to the various RoutingPorts via a helper handler + + + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Tx.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Tx.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Tx.ComPorts; } } + public int NumberOfComPorts { get { return Tx.NumberOfComPorts; } } + #endregion + + #region ICec Members + public Cec StreamCec { get { return Tx.StreamCec; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k202CController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k202CController.cs new file mode 100644 index 00000000..b8061ad9 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k202CController.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM +{ + using eVst = Crestron.SimplSharpPro.DeviceSupport.eX02VideoSourceType; + using eAst = Crestron.SimplSharpPro.DeviceSupport.eX02AudioSourceType; + + public class DmTx4k202CController : DmTxControllerBase, ITxRouting, IHasFeedback, + IIROutputPorts, IComPorts + { + public DmTx4kX02CBase Tx { get; private set; } + + public RoutingInputPortWithVideoStatuses HdmiIn1 { get; private set; } + public RoutingInputPortWithVideoStatuses HdmiIn2 { get; private set; } + public RoutingOutputPort DmOut { get; private set; } + public RoutingOutputPort HdmiLoopOut { get; private set; } + + public override StringFeedback ActiveVideoInputFeedback { get; protected set; } + public IntFeedback VideoSourceNumericFeedback { get; protected set; } + public IntFeedback AudioSourceNumericFeedback { get; protected set; } + public IntFeedback HdmiIn1HdcpCapabilityFeedback { get; protected set; } + public IntFeedback HdmiIn2HdcpCapabilityFeedback { get; protected set; } + + //public override IntFeedback HdcpSupportAllFeedback { get; protected set; } + //public override ushort HdcpSupportCapability { get; protected set; } + + /// + /// Helps get the "real" inputs, including when in Auto + /// + public Crestron.SimplSharpPro.DeviceSupport.eX02VideoSourceType ActualActiveVideoInput + { + get + { + if (Tx.VideoSourceFeedback != eVst.Auto) + return Tx.VideoSourceFeedback; + else // auto + { + if (Tx.HdmiInputs[1].SyncDetectedFeedback.BoolValue) + return eVst.Hdmi1; + else if (Tx.HdmiInputs[2].SyncDetectedFeedback.BoolValue) + return eVst.Hdmi2; + else + return eVst.AllDisabled; + } + } + } + public RoutingPortCollection InputPorts + { + get + { + return new RoutingPortCollection + { + HdmiIn1, + HdmiIn2, + AnyVideoInput + }; + } + } + public RoutingPortCollection OutputPorts + { + get + { + return new RoutingPortCollection { DmOut, HdmiLoopOut }; + } + } + public DmTx4k202CController(string key, string name, DmTx4kX02CBase tx) + : base(key, name, tx) + { + Tx = tx; + + HdmiIn1 = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn1, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, eVst.Hdmi1, this, + VideoStatusHelper.GetHdmiInputStatusFuncs(tx.HdmiInputs[1])); + HdmiIn2 = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn2, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, eVst.Hdmi2, this, + VideoStatusHelper.GetHdmiInputStatusFuncs(tx.HdmiInputs[2])); + ActiveVideoInputFeedback = new StringFeedback("ActiveVideoInput", + () => ActualActiveVideoInput.ToString()); + + + + Tx.HdmiInputs[1].InputStreamChange += InputStreamChangeEvent; + Tx.HdmiInputs[2].InputStreamChange += InputStreamChangeEvent; + Tx.BaseEvent += Tx_BaseEvent; + + VideoSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.VideoSourceFeedback; + }); + AudioSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.AudioSourceFeedback; + }); + + HdmiIn1HdcpCapabilityFeedback = new IntFeedback("HdmiIn1HdcpCapability", () => + { + return (int)tx.HdmiInputs[1].HdcpCapabilityFeedback; + }); + + HdmiIn2HdcpCapabilityFeedback = new IntFeedback("HdmiIn2HdcpCapability", () => + { + return (int)tx.HdmiInputs[2].HdcpCapabilityFeedback; + }); + + HdcpSupportCapability = eHdcpCapabilityType.Hdcp2_2Support; + + var combinedFuncs = new VideoStatusFuncsWrapper + { + HdcpActiveFeedbackFunc = () => + (ActualActiveVideoInput == eVst.Hdmi1 + && tx.HdmiInputs[1].VideoAttributes.HdcpActiveFeedback.BoolValue) + || (ActualActiveVideoInput == eVst.Hdmi2 + && tx.HdmiInputs[2].VideoAttributes.HdcpActiveFeedback.BoolValue), + + HdcpStateFeedbackFunc = () => + { + if (ActualActiveVideoInput == eVst.Hdmi1) + return tx.HdmiInputs[1].VideoAttributes.HdcpStateFeedback.ToString(); + if (ActualActiveVideoInput == eVst.Hdmi2) + return tx.HdmiInputs[2].VideoAttributes.HdcpStateFeedback.ToString(); + return ""; + }, + + VideoResolutionFeedbackFunc = () => + { + if (ActualActiveVideoInput == eVst.Hdmi1) + return tx.HdmiInputs[1].VideoAttributes.GetVideoResolutionString(); + if (ActualActiveVideoInput == eVst.Hdmi2) + return tx.HdmiInputs[2].VideoAttributes.GetVideoResolutionString(); + return ""; + }, + VideoSyncFeedbackFunc = () => + (ActualActiveVideoInput == eVst.Hdmi1 + && tx.HdmiInputs[1].SyncDetectedFeedback.BoolValue) + || (ActualActiveVideoInput == eVst.Hdmi2 + && tx.HdmiInputs[2].SyncDetectedFeedback.BoolValue) + + }; + + AnyVideoInput = new RoutingInputPortWithVideoStatuses(DmPortName.AnyVideoIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.None, 0, this, combinedFuncs); + + DmOut = new RoutingOutputPort(DmPortName.DmOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, null, this); + HdmiLoopOut = new RoutingOutputPort(DmPortName.HdmiLoopOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + + AddToFeedbackList(ActiveVideoInputFeedback, VideoSourceNumericFeedback, AudioSourceNumericFeedback, + AnyVideoInput.VideoStatus.HasVideoStatusFeedback, AnyVideoInput.VideoStatus.HdcpActiveFeedback, + AnyVideoInput.VideoStatus.HdcpStateFeedback, AnyVideoInput.VideoStatus.VideoResolutionFeedback, + AnyVideoInput.VideoStatus.VideoSyncFeedback, HdmiIn1HdcpCapabilityFeedback, HdmiIn2HdcpCapabilityFeedback); + + // Set Ports for CEC + HdmiIn1.Port = Tx.HdmiInputs[1]; + HdmiIn2.Port = Tx.HdmiInputs[2]; + HdmiLoopOut.Port = Tx.HdmiOutput; + DmOut.Port = Tx.DmOutput; + } + + + + public override bool CustomActivate() + { + // Link up all of these damned events to the various RoutingPorts via a helper handler + Tx.HdmiInputs[1].InputStreamChange += (o, a) => FowardInputStreamChange(HdmiIn1, a.EventId); + Tx.HdmiInputs[1].VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(HdmiIn1, a.EventId); + + Tx.HdmiInputs[2].InputStreamChange += (o, a) => FowardInputStreamChange(HdmiIn2, a.EventId); + Tx.HdmiInputs[2].VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(HdmiIn2, a.EventId); + + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + public void ExecuteNumericSwitch(ushort input, ushort output, eRoutingSignalType type) + { + Debug.Console(2, this, "Executing Numeric Switch to input {0}.", input); + + if (type == eRoutingSignalType.Video) + { + switch (input) + { + case 0: + { + ExecuteSwitch(eVst.Auto, null, type); + break; + } + case 1: + { + ExecuteSwitch(HdmiIn1.Selector, null, type); + break; + } + case 2: + { + ExecuteSwitch(HdmiIn2.Selector, null, type); + break; + } + case 3: + { + ExecuteSwitch(eVst.AllDisabled, null, type); + break; + } + } + } + else if (type == eRoutingSignalType.Audio) + { + switch (input) + { + case 0: + { + ExecuteSwitch(eAst.Auto, null, type); + break; + } + case 1: + { + ExecuteSwitch(eAst.Hdmi1, null, type); + break; + } + case 2: + { + ExecuteSwitch(eAst.Hdmi2, null, type); + break; + } + case 3: + { + ExecuteSwitch(eAst.AllDisabled, null, type); + break; + } + } + } + } + + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) + { + if ((signalType | eRoutingSignalType.Video) == eRoutingSignalType.Video) + Tx.VideoSource = (eVst)inputSelector; + if ((signalType | eRoutingSignalType.Audio) == eRoutingSignalType.Audio) + Tx.AudioSource = (eAst)inputSelector; + } + + void InputStreamChangeEvent(EndpointInputStream inputStream, EndpointInputStreamEventArgs args) + { + Debug.Console(2, "{0} event {1} stream {2}", this.Tx.ToString(), inputStream.ToString(), args.EventId.ToString()); + + if (args.EventId == EndpointInputStreamEventIds.HdcpSupportOffFeedbackEventId) + { + if (inputStream == Tx.HdmiInputs[1]) + HdmiIn1HdcpCapabilityFeedback.FireUpdate(); + else if (inputStream == Tx.HdmiInputs[2]) + HdmiIn2HdcpCapabilityFeedback.FireUpdate(); + } + else if (args.EventId == EndpointInputStreamEventIds.HdcpSupportOnFeedbackEventId) + { + if (inputStream == Tx.HdmiInputs[1]) + HdmiIn1HdcpCapabilityFeedback.FireUpdate(); + else if (inputStream == Tx.HdmiInputs[2]) + HdmiIn2HdcpCapabilityFeedback.FireUpdate(); + } + } + + void Tx_BaseEvent(GenericBase device, BaseEventArgs args) + { + var id = args.EventId; + if (id == EndpointTransmitterBase.VideoSourceFeedbackEventId) + { + Debug.Console(2, this, " Video Source: {0}", Tx.VideoSourceFeedback); + VideoSourceNumericFeedback.FireUpdate(); + ActiveVideoInputFeedback.FireUpdate(); + } + + // ------------------------------ incomplete ----------------------------------------- + else if (id == EndpointTransmitterBase.AudioSourceFeedbackEventId) + { + Debug.Console(2, this, " Audio Source: {0}", Tx.AudioSourceFeedback); + AudioSourceNumericFeedback.FireUpdate(); + } + } + + /// + /// Relays the input stream change to the appropriate RoutingInputPort. + /// + void FowardInputStreamChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + if (eventId == EndpointInputStreamEventIds.SyncDetectedFeedbackEventId) + { + inputPort.VideoStatus.VideoSyncFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoSyncFeedback.FireUpdate(); + } + } + + /// + /// Relays the VideoAttributes change to a RoutingInputPort + /// + void ForwardVideoAttributeChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + //// LOCATION: Crestron.SimplSharpPro.DM.VideoAttributeEventIds + //Debug.Console(2, this, "VideoAttributes_AttributeChange event id={0} from {1}", + // args.EventId, (sender as VideoAttributesEnhanced).Owner.GetType()); + switch (eventId) + { + case VideoAttributeEventIds.HdcpActiveFeedbackEventId: + inputPort.VideoStatus.HdcpActiveFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpActiveFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HdcpStateFeedbackEventId: + inputPort.VideoStatus.HdcpStateFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpStateFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HorizontalResolutionFeedbackEventId: + case VideoAttributeEventIds.VerticalResolutionFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.FramesPerSecondFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + } + } + + + + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Tx.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Tx.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Tx.ComPorts; } } + public int NumberOfComPorts { get { return Tx.NumberOfComPorts; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k302CController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k302CController.cs new file mode 100644 index 00000000..b086e430 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4k302CController.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM +{ + using eVst = Crestron.SimplSharpPro.DeviceSupport.eX02VideoSourceType; + using eAst = Crestron.SimplSharpPro.DeviceSupport.eX02AudioSourceType; + + public class DmTx4k302CController : DmTxControllerBase, ITxRouting, IHasFeedback, + IIROutputPorts, IComPorts + { + public DmTx4k302C Tx { get; private set; } + + public RoutingInputPortWithVideoStatuses HdmiIn1 { get; private set; } + public RoutingInputPortWithVideoStatuses HdmiIn2 { get; private set; } + public RoutingInputPortWithVideoStatuses VgaIn { get; private set; } + public RoutingOutputPort DmOut { get; private set; } + public RoutingOutputPort HdmiLoopOut { get; private set; } + + public override StringFeedback ActiveVideoInputFeedback { get; protected set; } + public IntFeedback VideoSourceNumericFeedback { get; protected set; } + public IntFeedback AudioSourceNumericFeedback { get; protected set; } + public IntFeedback HdmiIn1HdcpCapabilityFeedback { get; protected set; } + public IntFeedback HdmiIn2HdcpCapabilityFeedback { get; protected set; } + + //public override IntFeedback HdcpSupportAllFeedback { get; protected set; } + //public override ushort HdcpSupportCapability { get; protected set; } + + /// + /// Helps get the "real" inputs, including when in Auto + /// + public Crestron.SimplSharpPro.DeviceSupport.eX02VideoSourceType ActualActiveVideoInput + { + get + { + if (Tx.VideoSourceFeedback != eVst.Auto) + return Tx.VideoSourceFeedback; + else // auto + { + if (Tx.HdmiInputs[1].SyncDetectedFeedback.BoolValue) + return eVst.Hdmi1; + else if (Tx.HdmiInputs[2].SyncDetectedFeedback.BoolValue) + return eVst.Hdmi2; + else if (Tx.VgaInput.SyncDetectedFeedback.BoolValue) + return eVst.Vga; + else + return eVst.AllDisabled; + } + } + } + public RoutingPortCollection InputPorts + { + get + { + return new RoutingPortCollection + { + HdmiIn1, + HdmiIn2, + VgaIn, + AnyVideoInput + }; + } + } + public RoutingPortCollection OutputPorts + { + get + { + return new RoutingPortCollection { DmOut, HdmiLoopOut }; + } + } + public DmTx4k302CController(string key, string name, DmTx4k302C tx) + : base(key, name, tx) + { + Tx = tx; + + HdmiIn1 = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn1, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, eVst.Hdmi1, this, + VideoStatusHelper.GetHdmiInputStatusFuncs(tx.HdmiInputs[1])); + HdmiIn2 = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn2, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, eVst.Hdmi2, this, + VideoStatusHelper.GetHdmiInputStatusFuncs(tx.HdmiInputs[2])); + VgaIn = new RoutingInputPortWithVideoStatuses(DmPortName.VgaIn, + eRoutingSignalType.Video, eRoutingPortConnectionType.Vga, eVst.Vga, this, + VideoStatusHelper.GetVgaInputStatusFuncs(tx.VgaInput)); + ActiveVideoInputFeedback = new StringFeedback("ActiveVideoInput", + () => ActualActiveVideoInput.ToString()); + + Tx.HdmiInputs[1].InputStreamChange += InputStreamChangeEvent; + Tx.HdmiInputs[2].InputStreamChange += InputStreamChangeEvent; + Tx.BaseEvent += Tx_BaseEvent; + + VideoSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.VideoSourceFeedback; + }); + AudioSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.AudioSourceFeedback; + }); + + HdmiIn1HdcpCapabilityFeedback = new IntFeedback("HdmiIn1HdcpCapability", () => + { + return (int)tx.HdmiInputs[1].HdcpCapabilityFeedback; + }); + + HdmiIn2HdcpCapabilityFeedback = new IntFeedback("HdmiIn2HdcpCapability", () => + { + return (int)tx.HdmiInputs[2].HdcpCapabilityFeedback; + }); + + HdcpSupportCapability = eHdcpCapabilityType.Hdcp2_2Support; + + + var combinedFuncs = new VideoStatusFuncsWrapper + { + HdcpActiveFeedbackFunc = () => + (ActualActiveVideoInput == eVst.Hdmi1 + && tx.HdmiInputs[1].VideoAttributes.HdcpActiveFeedback.BoolValue) + || (ActualActiveVideoInput == eVst.Hdmi2 + && tx.HdmiInputs[2].VideoAttributes.HdcpActiveFeedback.BoolValue), + + HdcpStateFeedbackFunc = () => + { + if (ActualActiveVideoInput == eVst.Hdmi1) + return tx.HdmiInputs[1].VideoAttributes.HdcpStateFeedback.ToString(); + if (ActualActiveVideoInput == eVst.Hdmi2) + return tx.HdmiInputs[2].VideoAttributes.HdcpStateFeedback.ToString(); + return ""; + }, + + VideoResolutionFeedbackFunc = () => + { + if (ActualActiveVideoInput == eVst.Hdmi1) + return tx.HdmiInputs[1].VideoAttributes.GetVideoResolutionString(); + if (ActualActiveVideoInput == eVst.Hdmi2) + return tx.HdmiInputs[2].VideoAttributes.GetVideoResolutionString(); + if (ActualActiveVideoInput == eVst.Vga) + return tx.VgaInput.VideoAttributes.GetVideoResolutionString(); + return ""; + }, + VideoSyncFeedbackFunc = () => + (ActualActiveVideoInput == eVst.Hdmi1 + && tx.HdmiInputs[1].SyncDetectedFeedback.BoolValue) + || (ActualActiveVideoInput == eVst.Hdmi2 + && tx.HdmiInputs[2].SyncDetectedFeedback.BoolValue) + || (ActualActiveVideoInput == eVst.Vga + && tx.VgaInput.SyncDetectedFeedback.BoolValue) + + }; + + AnyVideoInput = new RoutingInputPortWithVideoStatuses(DmPortName.AnyVideoIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.None, 0, this, combinedFuncs); + + DmOut = new RoutingOutputPort(DmPortName.DmOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, null, this); + HdmiLoopOut = new RoutingOutputPort(DmPortName.HdmiLoopOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + + AddToFeedbackList(ActiveVideoInputFeedback, VideoSourceNumericFeedback, AudioSourceNumericFeedback, + AnyVideoInput.VideoStatus.HasVideoStatusFeedback, AnyVideoInput.VideoStatus.HdcpActiveFeedback, + AnyVideoInput.VideoStatus.HdcpStateFeedback, AnyVideoInput.VideoStatus.VideoResolutionFeedback, + AnyVideoInput.VideoStatus.VideoSyncFeedback, HdmiIn1HdcpCapabilityFeedback, HdmiIn2HdcpCapabilityFeedback); + + // Set Ports for CEC + HdmiIn1.Port = Tx.HdmiInputs[1]; + HdmiIn2.Port = Tx.HdmiInputs[2]; + HdmiLoopOut.Port = Tx.HdmiOutput; + DmOut.Port = Tx.DmOutput; + } + + + + public override bool CustomActivate() + { + // Link up all of these damned events to the various RoutingPorts via a helper handler + Tx.HdmiInputs[1].InputStreamChange += (o, a) => FowardInputStreamChange(HdmiIn1, a.EventId); + Tx.HdmiInputs[1].VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(HdmiIn1, a.EventId); + + Tx.HdmiInputs[2].InputStreamChange += (o, a) => FowardInputStreamChange(HdmiIn2, a.EventId); + Tx.HdmiInputs[2].VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(HdmiIn2, a.EventId); + + Tx.VgaInput.InputStreamChange += (o, a) => FowardInputStreamChange(VgaIn, a.EventId); + Tx.VgaInput.VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(VgaIn, a.EventId); + + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + public void ExecuteNumericSwitch(ushort input, ushort output, eRoutingSignalType type) + { + Debug.Console(2, this, "Executing Numeric Switch to input {0}.", input); + + if (type == eRoutingSignalType.Video) + { + switch (input) + { + case 0: + { + ExecuteSwitch(eVst.Auto, null, type); + break; + } + case 1: + { + ExecuteSwitch(HdmiIn1.Selector, null, type); + break; + } + case 2: + { + ExecuteSwitch(HdmiIn2.Selector, null, type); + break; + } + case 3: + { + ExecuteSwitch(VgaIn.Selector, null, type); + break; + } + case 4: + { + ExecuteSwitch(eVst.AllDisabled, null, type); + break; + } + } + } + else if (type == eRoutingSignalType.Audio) + { + switch (input) + { + case 0: + { + ExecuteSwitch(eAst.Auto, null, type); + break; + } + case 1: + { + ExecuteSwitch(eAst.Hdmi1, null, type); + break; + } + case 2: + { + ExecuteSwitch(eAst.Hdmi2, null, type); + break; + } + case 3: + { + ExecuteSwitch(eAst.AudioIn, null, type); + break; + } + case 4: + { + ExecuteSwitch(eAst.AllDisabled, null, type); + break; + } + } + } + } + + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) + { + if ((signalType | eRoutingSignalType.Video) == eRoutingSignalType.Video) + Tx.VideoSource = (eVst)inputSelector; + if ((signalType | eRoutingSignalType.Audio) == eRoutingSignalType.Audio) + Tx.AudioSource = (eAst)inputSelector; + } + + void InputStreamChangeEvent(EndpointInputStream inputStream, EndpointInputStreamEventArgs args) + { + Debug.Console(2, "{0} event {1} stream {2}", this.Tx.ToString(), inputStream.ToString(), args.EventId.ToString()); + + if (args.EventId == EndpointInputStreamEventIds.HdcpSupportOffFeedbackEventId) + { + if (inputStream == Tx.HdmiInputs[1]) + HdmiIn1HdcpCapabilityFeedback.FireUpdate(); + else if (inputStream == Tx.HdmiInputs[2]) + HdmiIn2HdcpCapabilityFeedback.FireUpdate(); + } + else if (args.EventId == EndpointInputStreamEventIds.HdcpSupportOnFeedbackEventId) + { + if (inputStream == Tx.HdmiInputs[1]) + HdmiIn1HdcpCapabilityFeedback.FireUpdate(); + else if (inputStream == Tx.HdmiInputs[2]) + HdmiIn2HdcpCapabilityFeedback.FireUpdate(); + } + } + + void Tx_BaseEvent(GenericBase device, BaseEventArgs args) + { + var id = args.EventId; + if (id == EndpointTransmitterBase.VideoSourceFeedbackEventId) + { + Debug.Console(2, this, " Video Source: {0}", Tx.VideoSourceFeedback); + VideoSourceNumericFeedback.FireUpdate(); + ActiveVideoInputFeedback.FireUpdate(); + } + + // ------------------------------ incomplete ----------------------------------------- + else if (id == EndpointTransmitterBase.AudioSourceFeedbackEventId) + { + Debug.Console(2, this, " Audio Source: {0}", Tx.AudioSourceFeedback); + AudioSourceNumericFeedback.FireUpdate(); + } + } + + /// + /// Relays the input stream change to the appropriate RoutingInputPort. + /// + void FowardInputStreamChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + if (eventId == EndpointInputStreamEventIds.SyncDetectedFeedbackEventId) + { + inputPort.VideoStatus.VideoSyncFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoSyncFeedback.FireUpdate(); + } + } + + /// + /// Relays the VideoAttributes change to a RoutingInputPort + /// + void ForwardVideoAttributeChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + //// LOCATION: Crestron.SimplSharpPro.DM.VideoAttributeEventIds + //Debug.Console(2, this, "VideoAttributes_AttributeChange event id={0} from {1}", + // args.EventId, (sender as VideoAttributesEnhanced).Owner.GetType()); + switch (eventId) + { + case VideoAttributeEventIds.HdcpActiveFeedbackEventId: + inputPort.VideoStatus.HdcpActiveFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpActiveFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HdcpStateFeedbackEventId: + inputPort.VideoStatus.HdcpStateFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpStateFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HorizontalResolutionFeedbackEventId: + case VideoAttributeEventIds.VerticalResolutionFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.FramesPerSecondFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + } + } + + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Tx.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Tx.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Tx.ComPorts; } } + public int NumberOfComPorts { get { return Tx.NumberOfComPorts; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4kz302CController.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4kz302CController.cs new file mode 100644 index 00000000..26dcb603 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTx4kz302CController.cs @@ -0,0 +1,340 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM +{ + using eVst = Crestron.SimplSharpPro.DeviceSupport.eX02VideoSourceType; + using eAst = Crestron.SimplSharpPro.DeviceSupport.eX02AudioSourceType; + + public class DmTx4kz302CController : DmTxControllerBase, ITxRouting, IHasFeedback, + IIROutputPorts, IComPorts + { + public DmTx4kz302C Tx { get; private set; } + + public RoutingInputPortWithVideoStatuses HdmiIn1 { get; private set; } + public RoutingInputPortWithVideoStatuses HdmiIn2 { get; private set; } + public RoutingInputPortWithVideoStatuses DisplayPortIn { get; private set; } + public RoutingOutputPort DmOut { get; private set; } + public RoutingOutputPort HdmiLoopOut { get; private set; } + + public override StringFeedback ActiveVideoInputFeedback { get; protected set; } + public IntFeedback VideoSourceNumericFeedback { get; protected set; } + public IntFeedback AudioSourceNumericFeedback { get; protected set; } + public IntFeedback HdmiIn1HdcpCapabilityFeedback { get; protected set; } + public IntFeedback HdmiIn2HdcpCapabilityFeedback { get; protected set; } + + //public override IntFeedback HdcpSupportAllFeedback { get; protected set; } + //public override ushort HdcpSupportCapability { get; protected set; } + + /// + /// Helps get the "real" inputs, including when in Auto + /// + public Crestron.SimplSharpPro.DeviceSupport.eX02VideoSourceType ActualActiveVideoInput + { + get + { + if (Tx.VideoSourceFeedback != eVst.Auto) + return Tx.VideoSourceFeedback; + else // auto + { + if (Tx.HdmiInputs[1].SyncDetectedFeedback.BoolValue) + return eVst.Hdmi1; + else if (Tx.HdmiInputs[2].SyncDetectedFeedback.BoolValue) + return eVst.Hdmi2; + else if (Tx.DisplayPortInput.SyncDetectedFeedback.BoolValue) + return eVst.Vga; + else + return eVst.AllDisabled; + } + } + } + public RoutingPortCollection InputPorts + { + get + { + return new RoutingPortCollection + { + HdmiIn1, + HdmiIn2, + DisplayPortIn, + AnyVideoInput + }; + } + } + public RoutingPortCollection OutputPorts + { + get + { + return new RoutingPortCollection { DmOut, HdmiLoopOut }; + } + } + public DmTx4kz302CController(string key, string name, DmTx4kz302C tx) + : base(key, name, tx) + { + Tx = tx; + + HdmiIn1 = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn1, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, eVst.Hdmi1, this, + VideoStatusHelper.GetHdmiInputStatusFuncs(tx.HdmiInputs[1])); + HdmiIn2 = new RoutingInputPortWithVideoStatuses(DmPortName.HdmiIn2, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, eVst.Hdmi2, this, + VideoStatusHelper.GetHdmiInputStatusFuncs(tx.HdmiInputs[2])); + DisplayPortIn = new RoutingInputPortWithVideoStatuses(DmPortName.VgaIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DisplayPort, eVst.DisplayPort, this, + VideoStatusHelper.GetDisplayPortInputStatusFuncs(tx.DisplayPortInput)); + ActiveVideoInputFeedback = new StringFeedback("ActiveVideoInput", + () => ActualActiveVideoInput.ToString()); + + Tx.HdmiInputs[1].InputStreamChange += InputStreamChangeEvent; + Tx.HdmiInputs[2].InputStreamChange += InputStreamChangeEvent; + Tx.BaseEvent += Tx_BaseEvent; + + VideoSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.VideoSourceFeedback; + }); + AudioSourceNumericFeedback = new IntFeedback(() => + { + return (int)Tx.AudioSourceFeedback; + }); + + HdmiIn1HdcpCapabilityFeedback = new IntFeedback("HdmiIn1HdcpCapability", () => + { + return (int)tx.HdmiInputs[1].HdcpCapabilityFeedback; + }); + + HdmiIn2HdcpCapabilityFeedback = new IntFeedback("HdmiIn2HdcpCapability", () => + { + return (int)tx.HdmiInputs[2].HdcpCapabilityFeedback; + }); + + HdcpSupportCapability = eHdcpCapabilityType.Hdcp2_2Support; + + + var combinedFuncs = new VideoStatusFuncsWrapper + { + HdcpActiveFeedbackFunc = () => + (ActualActiveVideoInput == eVst.Hdmi1 + && tx.HdmiInputs[1].VideoAttributes.HdcpActiveFeedback.BoolValue) + || (ActualActiveVideoInput == eVst.Hdmi2 + && tx.HdmiInputs[2].VideoAttributes.HdcpActiveFeedback.BoolValue), + + HdcpStateFeedbackFunc = () => + { + if (ActualActiveVideoInput == eVst.Hdmi1) + return tx.HdmiInputs[1].VideoAttributes.HdcpStateFeedback.ToString(); + if (ActualActiveVideoInput == eVst.Hdmi2) + return tx.HdmiInputs[2].VideoAttributes.HdcpStateFeedback.ToString(); + return ""; + }, + + VideoResolutionFeedbackFunc = () => + { + if (ActualActiveVideoInput == eVst.Hdmi1) + return tx.HdmiInputs[1].VideoAttributes.GetVideoResolutionString(); + if (ActualActiveVideoInput == eVst.Hdmi2) + return tx.HdmiInputs[2].VideoAttributes.GetVideoResolutionString(); + if (ActualActiveVideoInput == eVst.Vga) + return tx.DisplayPortInput.VideoAttributes.GetVideoResolutionString(); + return ""; + }, + VideoSyncFeedbackFunc = () => + (ActualActiveVideoInput == eVst.Hdmi1 + && tx.HdmiInputs[1].SyncDetectedFeedback.BoolValue) + || (ActualActiveVideoInput == eVst.Hdmi2 + && tx.HdmiInputs[2].SyncDetectedFeedback.BoolValue) + || (ActualActiveVideoInput == eVst.Vga + && tx.DisplayPortInput.SyncDetectedFeedback.BoolValue) + + }; + + AnyVideoInput = new RoutingInputPortWithVideoStatuses(DmPortName.AnyVideoIn, + eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.None, 0, this, combinedFuncs); + + DmOut = new RoutingOutputPort(DmPortName.DmOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DmCat, null, this); + HdmiLoopOut = new RoutingOutputPort(DmPortName.HdmiLoopOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + + AddToFeedbackList(ActiveVideoInputFeedback, VideoSourceNumericFeedback, AudioSourceNumericFeedback, + AnyVideoInput.VideoStatus.HasVideoStatusFeedback, AnyVideoInput.VideoStatus.HdcpActiveFeedback, + AnyVideoInput.VideoStatus.HdcpStateFeedback, AnyVideoInput.VideoStatus.VideoResolutionFeedback, + AnyVideoInput.VideoStatus.VideoSyncFeedback, HdmiIn1HdcpCapabilityFeedback, HdmiIn2HdcpCapabilityFeedback); + + // Set Ports for CEC + HdmiIn1.Port = Tx.HdmiInputs[1]; + HdmiIn2.Port = Tx.HdmiInputs[2]; + HdmiLoopOut.Port = Tx.HdmiOutput; + DmOut.Port = Tx.DmOutput; + } + + + + public override bool CustomActivate() + { + // Link up all of these damned events to the various RoutingPorts via a helper handler + Tx.HdmiInputs[1].InputStreamChange += (o, a) => FowardInputStreamChange(HdmiIn1, a.EventId); + Tx.HdmiInputs[1].VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(HdmiIn1, a.EventId); + + Tx.HdmiInputs[2].InputStreamChange += (o, a) => FowardInputStreamChange(HdmiIn2, a.EventId); + Tx.HdmiInputs[2].VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(HdmiIn2, a.EventId); + + Tx.DisplayPortInput.InputStreamChange += (o, a) => FowardInputStreamChange(DisplayPortIn, a.EventId); + Tx.DisplayPortInput.VideoAttributes.AttributeChange += (o, a) => ForwardVideoAttributeChange(DisplayPortIn, a.EventId); + + // Base does register and sets up comm monitoring. + return base.CustomActivate(); + } + + public void ExecuteNumericSwitch(ushort input, ushort output, eRoutingSignalType type) + { + Debug.Console(2, this, "Executing Numeric Switch to input {0}.", input); + + switch (input) + { + case 0: + { + ExecuteSwitch(eVst.Auto, null, eRoutingSignalType.AudioVideo); + break; + } + case 1: + { + ExecuteSwitch(HdmiIn1.Selector, null, eRoutingSignalType.AudioVideo); + break; + } + case 2: + { + ExecuteSwitch(HdmiIn2.Selector, null, eRoutingSignalType.AudioVideo); + break; + } + case 3: + { + ExecuteSwitch(DisplayPortIn.Selector, null, eRoutingSignalType.AudioVideo); + break; + } + case 4: + { + ExecuteSwitch(eVst.AllDisabled, null, eRoutingSignalType.AudioVideo); + break; + } + } + + + } + + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) + { + if ((signalType | eRoutingSignalType.Video) == eRoutingSignalType.Video) + Tx.VideoSource = (eVst)inputSelector; + + // NOTE: It's possible that this particular TX model may not like the AudioSource property being set. + // The SIMPL definition only shows a single analog for AudioVideo Source + if ((signalType | eRoutingSignalType.Audio) == eRoutingSignalType.Audio) + Tx.AudioSource = (eAst)inputSelector; + } + + void InputStreamChangeEvent(EndpointInputStream inputStream, EndpointInputStreamEventArgs args) + { + Debug.Console(2, "{0} event {1} stream {2}", this.Tx.ToString(), inputStream.ToString(), args.EventId.ToString()); + + if (args.EventId == EndpointInputStreamEventIds.HdcpSupportOffFeedbackEventId) + { + if (inputStream == Tx.HdmiInputs[1]) + HdmiIn1HdcpCapabilityFeedback.FireUpdate(); + else if (inputStream == Tx.HdmiInputs[2]) + HdmiIn2HdcpCapabilityFeedback.FireUpdate(); + } + else if (args.EventId == EndpointInputStreamEventIds.HdcpSupportOnFeedbackEventId) + { + if (inputStream == Tx.HdmiInputs[1]) + HdmiIn1HdcpCapabilityFeedback.FireUpdate(); + else if (inputStream == Tx.HdmiInputs[2]) + HdmiIn2HdcpCapabilityFeedback.FireUpdate(); + } + } + + void Tx_BaseEvent(GenericBase device, BaseEventArgs args) + { + var id = args.EventId; + if (id == EndpointTransmitterBase.VideoSourceFeedbackEventId) + { + Debug.Console(2, this, " Video Source: {0}", Tx.VideoSourceFeedback); + VideoSourceNumericFeedback.FireUpdate(); + ActiveVideoInputFeedback.FireUpdate(); + } + + // ------------------------------ incomplete ----------------------------------------- + else if (id == EndpointTransmitterBase.AudioSourceFeedbackEventId) + { + Debug.Console(2, this, " Audio Source: {0}", Tx.AudioSourceFeedback); + AudioSourceNumericFeedback.FireUpdate(); + } + } + + /// + /// Relays the input stream change to the appropriate RoutingInputPort. + /// + void FowardInputStreamChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + if (eventId == EndpointInputStreamEventIds.SyncDetectedFeedbackEventId) + { + inputPort.VideoStatus.VideoSyncFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoSyncFeedback.FireUpdate(); + } + } + + /// + /// Relays the VideoAttributes change to a RoutingInputPort + /// + void ForwardVideoAttributeChange(RoutingInputPortWithVideoStatuses inputPort, int eventId) + { + //// LOCATION: Crestron.SimplSharpPro.DM.VideoAttributeEventIds + //Debug.Console(2, this, "VideoAttributes_AttributeChange event id={0} from {1}", + // args.EventId, (sender as VideoAttributesEnhanced).Owner.GetType()); + switch (eventId) + { + case VideoAttributeEventIds.HdcpActiveFeedbackEventId: + inputPort.VideoStatus.HdcpActiveFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpActiveFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HdcpStateFeedbackEventId: + inputPort.VideoStatus.HdcpStateFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.HdcpStateFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.HorizontalResolutionFeedbackEventId: + case VideoAttributeEventIds.VerticalResolutionFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + case VideoAttributeEventIds.FramesPerSecondFeedbackEventId: + inputPort.VideoStatus.VideoResolutionFeedback.FireUpdate(); + AnyVideoInput.VideoStatus.VideoResolutionFeedback.FireUpdate(); + break; + } + } + + + #region IIROutputPorts Members + public CrestronCollection IROutputPorts { get { return Tx.IROutputPorts; } } + public int NumberOfIROutputPorts { get { return Tx.NumberOfIROutputPorts; } } + #endregion + + #region IComPorts Members + public CrestronCollection ComPorts { get { return Tx.ComPorts; } } + public int NumberOfComPorts { get { return Tx.NumberOfComPorts; } } + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTxHelpers.cs b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTxHelpers.cs new file mode 100644 index 00000000..48507151 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Endpoints/Transmitters/DmTxHelpers.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.DM.Config; + +namespace PepperDash.Essentials.DM +{ + public class DmTxHelper + { + /// + /// A factory method for various DmTxControllers + /// + /// + /// + /// + /// + public static BasicDmTxControllerBase GetDmTxController(string key, string name, string typeName, DmTxPropertiesConfig props) + { + // switch on type name... later... + + typeName = typeName.ToLower(); + //uint ipid = Convert.ToUInt16(props.Id, 16); + var ipid = props.Control.IpIdInt; + var pKey = props.ParentDeviceKey.ToLower(); + + if (pKey == "processor") + { + // Catch constructor failures, mainly dues to IPID + try + { + if(typeName.StartsWith("dmTx200")) + return new DmTx200Controller(key, name, new DmTx200C2G(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmtx201")) + return new DmTx201XController(key, name, new DmTx201S(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmtx4k202")) + return new DmTx4k202CController(key, name, new DmTx4k202C(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmtx4kz202")) + return new DmTx4k202CController(key, name, new DmTx4kz202C(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmtx4k302")) + return new DmTx4k302CController(key, name, new DmTx4k302C(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmtx4kz302")) + return new DmTx4kz302CController(key, name, new DmTx4kz302C(ipid, Global.ControlSystem)); + if (typeName.StartsWith("dmtx401")) + return new DmTx401CController(key, name, new DmTx401C(ipid, Global.ControlSystem)); + Debug.Console(0, "{1} WARNING: Cannot create DM-TX of type: '{0}'", typeName, key); + } + catch (Exception e) + { + Debug.Console(0, "[{0}] WARNING: Cannot create DM-TX device: {1}", key, e.Message); + } + } + else + { + var parentDev = DeviceManager.GetDeviceForKey(pKey); + if (!(parentDev is DmChassisController)) + { + Debug.Console(0, "Cannot create DM device '{0}'. '{1}' is not a DM Chassis.", + key, pKey); + return null; + } + + // Get the Crestron chassis and link stuff up + var switchDev = parentDev as DmChassisController; + var chassis = switchDev.Chassis; + + var num = props.ParentInputNumber; + if (num <= 0 || num > chassis.NumberOfInputs) + { + Debug.Console(0, "Cannot create DM device '{0}'. Input number '{1}' is out of range", + key, num); + return null; + } + else + { + var controller = (parentDev as DmChassisController); + controller.TxDictionary.Add(num, key); + } + + // Catch constructor failures, mainly dues to IPID + try + { + // Must use different constructor for CPU3 chassis types. No IPID + if (chassis is DmMd8x8Cpu3 || chassis is DmMd16x16Cpu3 || + chassis is DmMd32x32Cpu3 || chassis is DmMd8x8Cpu3rps || + chassis is DmMd16x16Cpu3rps || chassis is DmMd32x32Cpu3rps) + { + if (typeName.StartsWith("dmTx200")) + return new DmTx200Controller(key, name, new DmTx200C2G(chassis.Inputs[num])); + if (typeName.StartsWith("dmtx201")) + return new DmTx201XController(key, name, new DmTx201C(chassis.Inputs[num])); + if (typeName.StartsWith("dmtx4k100")) + return new DmTx4k100Controller(key, name, new DmTx4K100C1G(chassis.Inputs[num])); + if (typeName.StartsWith("dmtx4k202")) + return new DmTx4k202CController(key, name, new DmTx4k202C(chassis.Inputs[num])); + if (typeName.StartsWith("dmtx4kz202")) + return new DmTx4k202CController(key, name, new DmTx4kz202C(chassis.Inputs[num])); + if (typeName.StartsWith("dmtx4k302")) + return new DmTx4k302CController(key, name, new DmTx4k302C(chassis.Inputs[num])); + if (typeName.StartsWith("dmtx4kz302")) + return new DmTx4kz302CController(key, name, new DmTx4kz302C(chassis.Inputs[num])); + if (typeName.StartsWith("dmtx401")) + return new DmTx401CController(key, name, new DmTx401C(chassis.Inputs[num])); + } + else + { + if (typeName.StartsWith("dmTx200")) + return new DmTx200Controller(key, name, new DmTx200C2G(ipid, chassis.Inputs[num])); + if (typeName.StartsWith("dmtx201")) + return new DmTx201XController(key, name, new DmTx201C(ipid, chassis.Inputs[num])); + if (typeName.StartsWith("dmtx4k100")) + return new DmTx4k100Controller(key, name, new DmTx4K100C1G(ipid, chassis.Inputs[num])); + if (typeName.StartsWith("dmtx4k202")) + return new DmTx4k202CController(key, name, new DmTx4k202C(ipid, chassis.Inputs[num])); + if (typeName.StartsWith("dmtx4kz202")) + return new DmTx4k202CController(key, name, new DmTx4kz202C(ipid, chassis.Inputs[num])); + if (typeName.StartsWith("dmtx4k302")) + return new DmTx4k302CController(key, name, new DmTx4k302C(ipid, chassis.Inputs[num])); + if (typeName.StartsWith("dmtx4kz302")) + return new DmTx4kz302CController(key, name, new DmTx4kz302C(ipid, chassis.Inputs[num])); + if (typeName.StartsWith("dmtx401")) + return new DmTx401CController(key, name, new DmTx401C(ipid, chassis.Inputs[num])); + } + } + catch (Exception e) + { + Debug.Console(0, "[{0}] WARNING: Cannot create DM-TX device: {1}", key, e.Message); + } + } + + return null; + } + } + + public abstract class BasicDmTxControllerBase : CrestronGenericBaseDevice + { + public BasicDmTxControllerBase(string key, string name, GenericBase hardware) + : base(key, name, hardware) + { + + } + } + + /// + /// + /// + public abstract class DmTxControllerBase : BasicDmTxControllerBase + { + public virtual void SetPortHdcpCapability(eHdcpCapabilityType hdcpMode, uint port) { } + public virtual eHdcpCapabilityType HdcpSupportCapability { get; protected set; } + public abstract StringFeedback ActiveVideoInputFeedback { get; protected set; } + public RoutingInputPortWithVideoStatuses AnyVideoInput { get; protected set; } + + public DmTxControllerBase(string key, string name, EndpointTransmitterBase hardware) + : base(key, name, hardware) + { + // if wired to a chassis, skip registration step in base class + if (hardware.DMInput != null) + { + this.PreventRegistration = true; + } + AddToFeedbackList(ActiveVideoInputFeedback); + } + } + //public enum ePdtHdcpSupport + //{ + // HdcpOff = 0, + // Hdcp1 = 1, + // Hdcp2 = 2, + // Hdcp2_2= 3, + // Auto = 99 + //} +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Essentials_DM.csproj b/essentials-framework/Essentials DM/Essentials_DM/Essentials_DM.csproj new file mode 100644 index 00000000..e6dad5cb --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Essentials_DM.csproj @@ -0,0 +1,160 @@ + + + Release + AnyCPU + 9.0.30729 + 2.0 + {9199CE8A-0C9F-4952-8672-3EED798B284F} + Library + Properties + PepperDash.Essentials.DM + PepperDash_Essentials_DM + {0B4745B0-194B-4BB6-8E21-E9057CA92300};{4D628B5B-2FBC-4AA6-8C16-197242AEB884};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + WindowsCE + E2BECB1F-8C8C-41ba-B736-9BE7D946A398 + 5.0 + SmartDeviceProject1 + v3.5 + Windows CE + + + + + .allowedReferenceRelatedFileExtensions + true + full + false + bin\ + DEBUG;TRACE; + prompt + 4 + 512 + true + true + off + + + .allowedReferenceRelatedFileExtensions + none + true + bin\ + prompt + 4 + 512 + true + true + off + + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.DeviceSupport.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.DM.dll + + + False + ..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.UI.dll + + + + False + ..\..\references\PepperDash_Core.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpCustomAttributesInterface.dll + False + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpHelperInterface.dll + False + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpNewtonsoft.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpPro.exe + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + + + + + + + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5} + PepperDash_Essentials_Core + + + + + + + + + rem S# Pro preparation will execute after these operations + + \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Extensions.cs b/essentials-framework/Essentials DM/Essentials_DM/Extensions.cs new file mode 100644 index 00000000..c1ccfc5d --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Extensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; +using Crestron.SimplSharpPro.DM.Cards; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + public static class VideoAttributesBasicExtensions + { + public static string GetVideoResolutionString(this VideoAttributesBasic va) + { + ushort h = va.HorizontalResolutionFeedback.UShortValue; + ushort v = va.VerticalResolutionFeedback.UShortValue; + ushort r = va.FramesPerSecondFeedback.UShortValue; + if (h == 0 || v == 0) + return "n/a"; + else + return string.Format("{0}x{1}@{2}Hz", h, v, r); + } + } + + public static class AdvEndpointHdmiOutputExtensions + { + public static string GetVideoResolutionString(this AdvEndpointHdmiOutput va) + { + ushort h = va.HorizontalResolutionFeedback.UShortValue; + ushort v = va.VerticalResolutionFeedback.UShortValue; + ushort r = va.FramesPerSecondFeedback.UShortValue; + if (h == 0 || v == 0) + return "n/a"; + else + return string.Format("{0}x{1}@{2}Hz", h, v, r); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/IDmHdmiInputExtensions.cs b/essentials-framework/Essentials DM/Essentials_DM/IDmHdmiInputExtensions.cs new file mode 100644 index 00000000..a014e976 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/IDmHdmiInputExtensions.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + public static class IBasicDmInputExtensions + { + public static VideoStatusFuncsWrapper GetVideoStatusFuncsWrapper(this IBasicDMInput input) + { + var va = (input as IVideoAttributesEnhanced).VideoAttributes; + return new VideoStatusFuncsWrapper + { + HasVideoStatusFunc = () => true, + HdcpActiveFeedbackFunc = () => va.HdcpActiveFeedback.BoolValue, + HdcpStateFeedbackFunc = () => va.HdcpStateFeedback.ToString(), + VideoResolutionFeedbackFunc = () => + { + //var h = va.HorizontalResolutionFeedback.UShortValue; + //var v = va.VerticalResolutionFeedback.UShortValue; + //if (h == 0 || v == 0) + // return "---"; + return va.GetVideoResolutionString(); // h + "x" + v; + }, + VideoSyncFeedbackFunc = () => input.SyncDetectedFeedback.BoolValue + }; + } + } + + + public static class IEndpointHdmiInputExtensions + { + public static VideoStatusFuncsWrapper GetVideoStatusFuncsWrapper(this Crestron.SimplSharpPro.DM.Endpoints.EndpointHdmiInput input) + { + var va = (input as IVideoAttributesEnhanced).VideoAttributes; + return new VideoStatusFuncsWrapper + { + HasVideoStatusFunc = () => true, + HdcpActiveFeedbackFunc = () => va.HdcpActiveFeedback.BoolValue, + HdcpStateFeedbackFunc = () => va.HdcpStateFeedback.ToString(), + VideoResolutionFeedbackFunc = () => + { + //var h = va.HorizontalResolutionFeedback.UShortValue; + //var v = va.VerticalResolutionFeedback.UShortValue; + //if (h == 0 || v == 0) + // return "---"; + return va.GetVideoResolutionString(); // h + "x" + v; + }, + VideoSyncFeedbackFunc = () => input.SyncDetectedFeedback.BoolValue + }; + } + } + + +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/MOVE IBasicVideoStatusFeedbacks.cs b/essentials-framework/Essentials DM/Essentials_DM/MOVE IBasicVideoStatusFeedbacks.cs new file mode 100644 index 00000000..26b950ce --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/MOVE IBasicVideoStatusFeedbacks.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + public interface IBasicVideoStatusFeedbacks + { + BoolFeedback HasVideoStatusFeedback { get; } + BoolFeedback HdcpActiveFeedback { get; } + StringFeedback HdcpStateFeedback { get; } + StringFeedback VideoResolutionFeedback { get; } + BoolFeedback VideoSyncFeedback { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Nvx/NvxDirectorController.cs b/essentials-framework/Essentials DM/Essentials_DM/Nvx/NvxDirectorController.cs new file mode 100644 index 00000000..fa9749c5 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Nvx/NvxDirectorController.cs @@ -0,0 +1,746 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DM; +//using Crestron.SimplSharpPro.DM.Cards; +//using Crestron.SimplSharpPro.DM.Streaming; +//using Crestron.SimplSharpPro.DM.Endpoints; +//using Crestron.SimplSharpPro.DM.Endpoints.Receivers; + +//using PepperDash.Core; +//using PepperDash.Essentials.Core; +//using PepperDash.Essentials.DM.Cards; +//using PepperDash.Essentials.DM.Config; + +//namespace PepperDash.Essentials.DM +//{ +// /// +// /// Builds a controller for basic DM-RMCs with Com and IR ports and no control functions +// /// +// /// +// public class NvxDirectorController : CrestronGenericBaseDevice, IRoutingInputsOutputs, IRouting, IHasFeedback//, ICardPortsDevice +// { +// public NvxDirectorController Chassis { get; private set; } + +// // Feedbacks for EssentialDM +// public Dictionary VideoOutputFeedbacks { get; private set; } +// public Dictionary AudioOutputFeedbacks { get; private set; } +// public Dictionary VideoInputSyncFeedbacks { get; private set; } +// public Dictionary InputEndpointOnlineFeedbacks { get; private set; } +// public Dictionary OutputEndpointOnlineFeedbacks { get; private set; } +// public Dictionary InputNameFeedbacks { get; private set; } +// public Dictionary OutputNameFeedbacks { get; private set; } +// public Dictionary OutputVideoRouteNameFeedbacks { get; private set; } +// public Dictionary OutputAudioRouteNameFeedbacks { get; private set; } + + +// // Need a couple Lists of generic Backplane ports +// public RoutingPortCollection InputPorts { get; private set; } +// public RoutingPortCollection OutputPorts { get; private set; } + +// public Dictionary TxDictionary { get; set; } +// public Dictionary RxDictionary { get; set; } + +// //public Dictionary InputCards { get; private set; } +// //public Dictionary OutputCards { get; private set; } + +// public Dictionary InputNames { get; set; } +// public Dictionary OutputNames { get; set; } +// public Dictionary VolumeControls { get; private set; } + +// public const int RouteOffTime = 500; +// Dictionary RouteOffTimers = new Dictionary(); + +// /// +// /// Factory method to create a new chassis controller from config data. Limited to 8x8 right now +// /// +// public static NvxDirectorController GetNvxDirectorController(string key, string name, +// string type, DMChassisPropertiesConfig properties) +// { +// try +// { +// type = type.ToLower(); +// uint ipid = properties.Control.IpIdInt; // Convert.ToUInt16(properties.Id, 16); +// NvxDirectorController controller = null; + + +// if (type == "dmmd8x8") +// { +// controller = new NvxDirectorController(key, name, new DmMd8x8(ipid, Global.ControlSystem)); + +// // add the cards and port names +// foreach (var kvp in properties.InputSlots) +// controller.AddInputCard(kvp.Value, kvp.Key); +// foreach (var kvp in properties.OutputSlots) { +// controller.AddOutputCard(kvp.Value, kvp.Key); + +// } + +// foreach (var kvp in properties.VolumeControls) +// { +// // get the card +// // check it for an audio-compatible type +// // make a something-something that will make it work +// // retire to mountain village +// var outNum = kvp.Key; +// var card = controller.Chassis.Outputs[outNum].Card; +// Audio.Output audio = null; +// if (card is DmcHdo) +// audio = (card as DmcHdo).Audio; +// else if (card is Dmc4kHdo) +// audio = (card as Dmc4kHdo).Audio; +// if (audio == null) +// continue; +// // wire up the audio to something here... +// controller.AddVolumeControl(outNum, audio); +// } + +// controller.InputNames = properties.InputNames; +// controller.OutputNames = properties.OutputNames; +// return controller; +// } +// else if (type == "dmmd16x16") { +// controller = new NvxDirectorController(key, name, new DmMd16x16(ipid, Global.ControlSystem)); + +// // add the cards and port names +// foreach (var kvp in properties.InputSlots) +// controller.AddInputCard(kvp.Value, kvp.Key); +// foreach (var kvp in properties.OutputSlots) { +// controller.AddOutputCard(kvp.Value, kvp.Key); + +// } + +// foreach (var kvp in properties.VolumeControls) { +// // get the card +// // check it for an audio-compatible type +// // make a something-something that will make it work +// // retire to mountain village +// var outNum = kvp.Key; +// var card = controller.Chassis.Outputs[outNum].Card; +// Audio.Output audio = null; +// if (card is DmcHdo) +// audio = (card as DmcHdo).Audio; +// else if (card is Dmc4kHdo) +// audio = (card as Dmc4kHdo).Audio; +// if (audio == null) +// continue; +// // wire up the audio to something here... +// controller.AddVolumeControl(outNum, audio); +// } + +// controller.InputNames = properties.InputNames; +// controller.OutputNames = properties.OutputNames; +// return controller; +// } +// } +// catch (System.Exception e) +// { +// Debug.Console(0, "Error creating DM chassis:\r{0}", e); +// } +// return null; +// } + + +// /// +// /// +// /// +// /// +// /// +// /// +// public NvxDirectorController(string key, string name, DmMDMnxn chassis) +// : base(key, name, chassis) +// { +// Chassis = chassis; +// InputPorts = new RoutingPortCollection(); +// OutputPorts = new RoutingPortCollection(); +// VolumeControls = new Dictionary(); +// TxDictionary = new Dictionary(); +// RxDictionary = new Dictionary(); +// IsOnline.OutputChange += new EventHandler(IsOnline_OutputChange); +// //IsOnline.OutputChange += new EventHandler(this.IsOnline_OutputChange); +// Chassis.DMInputChange += new DMInputEventHandler(Chassis_DMInputChange); +// //Chassis.DMSystemChange += new DMSystemEventHandler(Chassis_DMSystemChange); +// Chassis.DMOutputChange += new DMOutputEventHandler(Chassis_DMOutputChange); +// VideoOutputFeedbacks = new Dictionary(); +// AudioOutputFeedbacks = new Dictionary(); +// VideoInputSyncFeedbacks = new Dictionary(); +// InputNameFeedbacks = new Dictionary(); +// OutputNameFeedbacks = new Dictionary(); +// OutputVideoRouteNameFeedbacks = new Dictionary(); +// OutputAudioRouteNameFeedbacks = new Dictionary(); +// InputEndpointOnlineFeedbacks = new Dictionary(); +// OutputEndpointOnlineFeedbacks = new Dictionary(); + +// for (uint x = 1; x <= Chassis.NumberOfOutputs; x++) +// { +// var tempX = x; + +// VideoOutputFeedbacks[tempX] = new IntFeedback(() => { +// if (Chassis.Outputs[tempX].VideoOutFeedback != null) { return (ushort)Chassis.Outputs[tempX].VideoOutFeedback.Number;} +// else { return 0; }; +// }); +// AudioOutputFeedbacks[tempX] = new IntFeedback(() => { +// if (Chassis.Outputs[tempX].AudioOutFeedback != null) { return (ushort)Chassis.Outputs[tempX].AudioOutFeedback.Number; } +// else { return 0; }; +// }); +// VideoInputSyncFeedbacks[tempX] = new BoolFeedback(() => { +// return Chassis.Inputs[tempX].VideoDetectedFeedback.BoolValue; +// }); +// InputNameFeedbacks[tempX] = new StringFeedback(() => { +// if (Chassis.Inputs[tempX].NameFeedback.StringValue != null) +// { +// return Chassis.Inputs[tempX].NameFeedback.StringValue; +// } +// else +// { +// return ""; +// } +// }); +// OutputNameFeedbacks[tempX] = new StringFeedback(() => { +// if (Chassis.Outputs[tempX].NameFeedback.StringValue != null) +// { +// return Chassis.Outputs[tempX].NameFeedback.StringValue; +// } +// else +// { +// return ""; +// } +// }); +// OutputVideoRouteNameFeedbacks[tempX] = new StringFeedback(() => +// { +// if (Chassis.Outputs[tempX].VideoOutFeedback != null) +// { +// return Chassis.Outputs[tempX].VideoOutFeedback.NameFeedback.StringValue; +// } +// else +// { +// return ""; +// } +// }); +// OutputAudioRouteNameFeedbacks[tempX] = new StringFeedback(() => +// { +// if (Chassis.Outputs[tempX].AudioOutFeedback != null) +// { +// return Chassis.Outputs[tempX].AudioOutFeedback.NameFeedback.StringValue; +// } +// else +// { +// return ""; + +// } +// }); +// InputEndpointOnlineFeedbacks[tempX] = new BoolFeedback(() => { return Chassis.Inputs[tempX].EndpointOnlineFeedback; }); + +// OutputEndpointOnlineFeedbacks[tempX] = new BoolFeedback(() => { return Chassis.Outputs[tempX].EndpointOnlineFeedback; }); +// } +// } + +// /// +// /// +// /// +// /// +// /// +// public void AddInputCard(string type, uint number) +// { +// Debug.Console(2, this, "Adding input card '{0}', slot {1}", type, number); + +// if (type == "dmcHd") +// { +// var inputCard = new DmcHd(number, this.Chassis); +// var cecPort = inputCard.HdmiInput as ICec; +// AddHdmiInCardPorts(number, cecPort); +// } +// else if (type == "dmcHdDsp") +// { +// var inputCard = new DmcHdDsp(number, this.Chassis); +// var cecPort = inputCard.HdmiInput as ICec; +// AddHdmiInCardPorts(number, cecPort); +// } +// else if (type == "dmc4kHd") +// { +// var inputCard = new Dmc4kHd(number, this.Chassis); +// var cecPort = inputCard.HdmiInput as ICec; +// AddHdmiInCardPorts(number, cecPort); +// } +// else if (type == "dmc4kHdDsp") +// { +// var inputCard = new Dmc4kHdDsp(number, this.Chassis); +// var cecPort = inputCard.HdmiInput as ICec; +// AddHdmiInCardPorts(number, cecPort); +// } +// else if (type == "dmc4kzHd") +// { +// var inputCard = new Dmc4kzHd(number, this.Chassis); +// var cecPort = inputCard.HdmiInput as ICec; +// AddHdmiInCardPorts(number, cecPort); +// } +// else if (type == "dmc4kzHdDsp") +// { +// var inputCard = new Dmc4kzHdDsp(number, this.Chassis); +// var cecPort = inputCard.HdmiInput as ICec; +// AddHdmiInCardPorts(number, cecPort); +// } +// else if (type == "dmcC") +// { +// new DmcC(number, this.Chassis); +// AddDmInCardPorts(number); +// } +// else if (type == "dmcCDsp") +// { +// new DmcCDsp(number, this.Chassis); +// AddDmInCardPorts(number); +// } +// else if (type == "dmc4kC") +// { +// new Dmc4kC(number, this.Chassis); +// AddDmInCardPorts(number); +// } +// else if (type == "dmc4kCDsp") +// { +// new Dmc4kCDsp(number, this.Chassis); +// AddDmInCardPorts(number); +// } +// else if (type == "dmc4kzC") +// { +// new Dmc4kzC(number, this.Chassis); +// AddDmInCardPorts(number); +// } +// else if (type == "dmc4kzCDsp") +// { +// new Dmc4kzCDsp(number, this.Chassis); +// AddDmInCardPorts(number); +// } +// else if (type == "dmcCat") +// { +// new DmcCat(number, this.Chassis); +// AddDmInCardPorts(number); +// } +// else if (type == "dmcCatDsp") +// { +// new DmcCatDsp(number, this.Chassis); +// AddDmInCardPorts(number); +// } +// else if (type == "dmcS") +// { +// new DmcS(number, Chassis); +// AddInputPortWithDebug(number, "dmIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmMmFiber); +// AddInCardHdmiAndAudioLoopPorts(number); +// } +// else if (type == "dmcSDsp") +// { +// new DmcSDsp(number, Chassis); +// AddInputPortWithDebug(number, "dmIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmMmFiber); +// AddInCardHdmiAndAudioLoopPorts(number); +// } +// else if (type == "dmcS2") +// { +// new DmcS2(number, Chassis); +// AddInputPortWithDebug(number, "dmIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmSmFiber); +// AddInCardHdmiAndAudioLoopPorts(number); +// } +// else if (type == "dmcS2Dsp") +// { +// new DmcS2Dsp(number, Chassis); +// AddInputPortWithDebug(number, "dmIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmSmFiber); +// AddInCardHdmiAndAudioLoopPorts(number); +// } +// else if (type == "dmcSdi") +// { +// new DmcSdi(number, Chassis); +// AddInputPortWithDebug(number, "sdiIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Sdi); +// AddOutputPortWithDebug(number, "sdiOut", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Sdi, null); +// AddInCardHdmiAndAudioLoopPorts(number); +// } +// else if (type == "dmcDvi") +// { +// new DmcDvi(number, Chassis); +// AddInputPortWithDebug(number, "dviIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Dvi); +// AddInputPortWithDebug(number, "audioIn", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio); +// AddInCardHdmiLoopPort(number); +// } +// else if (type == "dmcVga") +// { +// new DmcVga(number, Chassis); +// AddInputPortWithDebug(number, "vgaIn", eRoutingSignalType.Video, eRoutingPortConnectionType.Vga); +// AddInputPortWithDebug(number, "audioIn", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio); +// AddInCardHdmiLoopPort(number); +// } +// else if (type == "dmcVidBnc") +// { +// new DmcVidBnc(number, Chassis); +// AddInputPortWithDebug(number, "componentIn", eRoutingSignalType.Video, eRoutingPortConnectionType.Component); +// AddInputPortWithDebug(number, "audioIn", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio); +// AddInCardHdmiLoopPort(number); +// } +// else if (type == "dmcVidRcaA") +// { +// new DmcVidRcaA(number, Chassis); +// AddInputPortWithDebug(number, "componentIn", eRoutingSignalType.Video, eRoutingPortConnectionType.Component); +// AddInputPortWithDebug(number, "audioIn", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio); +// AddInCardHdmiLoopPort(number); +// } +// else if (type == "dmcVidRcaD") +// { +// new DmcVidRcaD(number, Chassis); +// AddInputPortWithDebug(number, "componentIn", eRoutingSignalType.Video, eRoutingPortConnectionType.Component); +// AddInputPortWithDebug(number, "audioIn", eRoutingSignalType.Audio, eRoutingPortConnectionType.DigitalAudio); +// AddInCardHdmiLoopPort(number); +// } +// else if (type == "dmcVid4") +// { +// new DmcVid4(number, Chassis); +// AddInputPortWithDebug(number, "compositeIn1", eRoutingSignalType.Video, eRoutingPortConnectionType.Composite); +// AddInputPortWithDebug(number, "compositeIn2", eRoutingSignalType.Video, eRoutingPortConnectionType.Composite); +// AddInputPortWithDebug(number, "compositeIn3", eRoutingSignalType.Video, eRoutingPortConnectionType.Composite); +// AddInputPortWithDebug(number, "compositeIn4", eRoutingSignalType.Video, eRoutingPortConnectionType.Composite); +// AddInCardHdmiLoopPort(number); +// } +// else if (type == "dmcStr") +// { +// new DmcStr(number, Chassis); +// AddInputPortWithDebug(number, "streamIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Streaming); +// AddInCardHdmiAndAudioLoopPorts(number); +// } +// } + +// void AddDmInCardPorts(uint number) +// { +// AddInputPortWithDebug(number, "dmIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmCat); +// AddInCardHdmiAndAudioLoopPorts(number); +// } + +// void AddHdmiInCardPorts(uint number, ICec cecPort) +// { +// AddInputPortWithDebug(number, "hdmiIn", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, cecPort); +// AddInCardHdmiAndAudioLoopPorts(number); +// } + +// void AddInCardHdmiAndAudioLoopPorts(uint number) +// { +// AddOutputPortWithDebug(number, "hdmiLoopOut", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null); +// AddOutputPortWithDebug(number, "audioLoopOut", eRoutingSignalType.Audio, eRoutingPortConnectionType.Hdmi, null); +// } + +// void AddInCardHdmiLoopPort(uint number) +// { +// AddOutputPortWithDebug(number, "hdmiLoopOut", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null); +// } + +// /// +// /// +// /// +// /// +// /// +// public void AddOutputCard(string type, uint number) +// { +// Debug.Console(2, this, "Adding output card '{0}', slot {1}", type, number); +// if (type == "dmc4kHdo") +// { +// var outputCard = new Dmc4kHdoSingle(number, Chassis); +// var cecPort1 = outputCard.Card1.HdmiOutput; +// var cecPort2 = outputCard.Card2.HdmiOutput; +// AddDmcHdoPorts(number, cecPort1, cecPort2); +// } +// else if (type == "dmcHdo") +// { +// var outputCard = new DmcHdoSingle(number, Chassis); +// var cecPort1 = outputCard.Card1.HdmiOutput; +// var cecPort2 = outputCard.Card2.HdmiOutput; +// AddDmcHdoPorts(number, cecPort1, cecPort2); +// } +// else if (type == "dmc4kCoHd") +// { +// var outputCard = new Dmc4kCoHdSingle(number, Chassis); +// var cecPort1 = outputCard.Card1.HdmiOutput; +// AddDmcCoPorts(number, cecPort1); +// } +// else if (type == "dmc4kzCoHd") +// { +// var outputCard = new Dmc4kzCoHdSingle(number, Chassis); +// var cecPort1 = outputCard.Card1.HdmiOutput; +// AddDmcCoPorts(number, cecPort1); +// } +// else if (type == "dmcCoHd") +// { +// var outputCard = new DmcCoHdSingle(number, Chassis); +// var cecPort1 = outputCard.Card1.HdmiOutput; +// AddDmcCoPorts(number, cecPort1); +// } +// else if (type == "dmCatoHd") +// { +// var outputCard = new DmcCatoHdSingle(number, Chassis); +// var cecPort1 = outputCard.Card1.HdmiOutput; +// AddDmcCoPorts(number, cecPort1); +// } +// else if (type == "dmcSoHd") +// { +// var outputCard = new DmcSoHdSingle(number, Chassis); +// var cecPort1 = outputCard.Card1.HdmiOutput; +// AddOutputPortWithDebug(number, "dmOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmMmFiber, 2 * (number - 1) + 1); +// AddOutputPortWithDebug(number, "hdmiOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2 * (number - 1) + 1, cecPort1); +// AddOutputPortWithDebug(number, "dmOut2", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmMmFiber, 2 * (number - 1) + 2); + +// } +// else if (type == "dmcS2oHd") +// { +// var outputCard = new DmcS2oHdSingle(number, Chassis); +// var cecPort1 = outputCard.Card1.HdmiOutput; +// AddOutputPortWithDebug(number, "dmOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmSmFiber, 2 * (number - 1) + 1); +// AddOutputPortWithDebug(number, "hdmiOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2 * (number - 1) + 1, cecPort1); +// AddOutputPortWithDebug(number, "dmOut2", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmSmFiber, 2 * (number - 1) + 2); +// } +// else if (type == "dmcStro") +// { +// var outputCard = new DmcStroSingle(number, Chassis); +// AddOutputPortWithDebug(number, "streamOut", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Streaming, 2 * (number - 1) + 1); +// } + +// else +// Debug.Console(1, this, " WARNING: Output card type '{0}' is not available", type); +// } + +// void AddDmcHdoPorts(uint number, ICec cecPort1, ICec cecPort2) +// { +// AddOutputPortWithDebug(number, "hdmiOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2 * (number - 1) + 1, cecPort1); +// AddOutputPortWithDebug(number, "audioOut1", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio, 2 * (number - 1) + 1); +// AddOutputPortWithDebug(number, "hdmiOut2", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2 * (number - 1) + 2, cecPort2); +// AddOutputPortWithDebug(number, "audioOut2", eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio, 2 * (number - 1) + 2); +// } + +// void AddDmcCoPorts(uint number, ICec cecPort1) +// { +// AddOutputPortWithDebug(number, "dmOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmCat, 2 * (number - 1) + 1); +// AddOutputPortWithDebug(number, "hdmiOut1", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2 * (number - 1) + 1, cecPort1); +// AddOutputPortWithDebug(number, "dmOut2", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.DmCat, 2 * (number - 1) + 2); +// } + + +// /// +// /// Adds InputPort +// /// +// void AddInputPortWithDebug(uint cardNum, string portName, eRoutingSignalType sigType, eRoutingPortConnectionType portType) +// { +// var portKey = string.Format("inputCard{0}--{1}", cardNum, portName); +// Debug.Console(2, this, "Adding input port '{0}'", portKey); +// var inputPort = new RoutingInputPort(portKey, sigType, portType, cardNum, this); + +// InputPorts.Add(inputPort); +// } + +// /// +// /// Adds InputPort and sets Port as ICec object +// /// +// void AddInputPortWithDebug(uint cardNum, string portName, eRoutingSignalType sigType, eRoutingPortConnectionType portType, ICec cecPort) +// { +// var portKey = string.Format("inputCard{0}--{1}", cardNum, portName); +// Debug.Console(2, this, "Adding input port '{0}'", portKey); +// var inputPort = new RoutingInputPort(portKey, sigType, portType, cardNum, this); + +// if (cecPort != null) +// inputPort.Port = cecPort; + +// InputPorts.Add(inputPort); +// } + +// /// +// /// Adds OutputPort +// /// +// void AddOutputPortWithDebug(uint cardNum, string portName, eRoutingSignalType sigType, eRoutingPortConnectionType portType, object selector) +// { +// var portKey = string.Format("outputCard{0}--{1}", cardNum, portName); +// Debug.Console(2, this, "Adding output port '{0}'", portKey); +// OutputPorts.Add(new RoutingOutputPort(portKey, sigType, portType, selector, this)); +// } + +// /// +// /// Adds OutputPort and sets Port as ICec object +// /// +// void AddOutputPortWithDebug(uint cardNum, string portName, eRoutingSignalType sigType, eRoutingPortConnectionType portType, object selector, ICec cecPort) +// { +// var portKey = string.Format("outputCard{0}--{1}", cardNum, portName); +// Debug.Console(2, this, "Adding output port '{0}'", portKey); +// var outputPort = new RoutingOutputPort(portKey, sigType, portType, selector, this); + +// if (cecPort != null) +// outputPort.Port = cecPort; + +// OutputPorts.Add(outputPort); +// } + +// /// +// /// +// /// +// void AddVolumeControl(uint number, Audio.Output audio) +// { +// VolumeControls.Add(number, new DmCardAudioOutputController(audio)); +// } + +// //public void SetInputHdcpSupport(uint input, ePdtHdcpSupport hdcpSetting) +// //{ + +// //} + + +// void Chassis_DMSystemChange(Switch device, DMSystemEventArgs args) { + +// } +// void Chassis_DMInputChange(Switch device, DMInputEventArgs args) { +// //Debug.Console(2, this, "DMSwitch:{0} Input:{1} Event:{2}'", this.Name, args.Number, args.EventId.ToString()); + +// switch (args.EventId) { +// case (DMInputEventIds.OnlineFeedbackEventId): { +// Debug.Console(2, this, "DMINput OnlineFeedbackEventId for input: {0}. State: {1}", args.Number, device.Inputs[args.Number].EndpointOnlineFeedback); +// InputEndpointOnlineFeedbacks[args.Number].FireUpdate(); +// break; +// } +// case (DMInputEventIds.VideoDetectedEventId): { +// Debug.Console(2, this, "DM Input {0} VideoDetectedEventId", args.Number); +// VideoInputSyncFeedbacks[args.Number].FireUpdate(); +// break; +// } +// case (DMInputEventIds.InputNameEventId): { +// Debug.Console(2, this, "DM Input {0} NameFeedbackEventId", args.Number); +// InputNameFeedbacks[args.Number].FireUpdate(); +// break; +// } +// } +// } +// /// +// /// +// void Chassis_DMOutputChange(Switch device, DMOutputEventArgs args) +// { + +// //This should be a switch case JTA 2018-07-02 +// var output = args.Number; +// if (args.EventId == DMOutputEventIds.VolumeEventId && +// VolumeControls.ContainsKey(output)) +// { +// VolumeControls[args.Number].VolumeEventFromChassis(); +// } +// else if (args.EventId == DMOutputEventIds.OnlineFeedbackEventId) +// { +// OutputEndpointOnlineFeedbacks[output].FireUpdate(); +// } +// else if (args.EventId == DMOutputEventIds.VideoOutEventId) +// { +// if (Chassis.Outputs[output].VideoOutFeedback != null) +// { +// Debug.Console(2, this, "DMSwitchVideo:{0} Routed Input:{1} Output:{2}'", this.Name, Chassis.Outputs[output].VideoOutFeedback.Number, output); +// } +// if (VideoOutputFeedbacks.ContainsKey(output)) +// { +// VideoOutputFeedbacks[output].FireUpdate(); + +// } +// if (OutputVideoRouteNameFeedbacks.ContainsKey(output)) +// { +// OutputVideoRouteNameFeedbacks[output].FireUpdate(); +// } +// } +// else if (args.EventId == DMOutputEventIds.AudioOutEventId) +// { +// if (Chassis.Outputs[output].AudioOutFeedback != null) +// { +// Debug.Console(2, this, "DMSwitchAudio:{0} Routed Input:{1} Output:{2}'", this.Name, Chassis.Outputs[output].AudioOutFeedback.Number, output); +// } +// if (AudioOutputFeedbacks.ContainsKey(output)) +// { +// AudioOutputFeedbacks[output].FireUpdate(); +// } +// } +// else if (args.EventId == DMOutputEventIds.OutputNameEventId) +// { +// Debug.Console(2, this, "DM Output {0} NameFeedbackEventId", output); +// OutputNameFeedbacks[output].FireUpdate(); +// } + +// } + +// /// +// /// +// /// +// /// +// void StartOffTimer(PortNumberType pnt) +// { +// if (RouteOffTimers.ContainsKey(pnt)) +// return; +// RouteOffTimers[pnt] = new CTimer(o => +// { +// ExecuteSwitch(0, pnt.Number, pnt.Type); +// }, RouteOffTime); +// } + + +// // Send out sigs when coming online +// void IsOnline_OutputChange(object sender, EventArgs e) +// { +// if (IsOnline.BoolValue) +// { +// if (InputNames != null) +// foreach (var kvp in InputNames) +// Chassis.Inputs[kvp.Key].Name.StringValue = kvp.Value; +// if (OutputNames != null) +// foreach(var kvp in OutputNames) +// Chassis.Outputs[kvp.Key].Name.StringValue = kvp.Value; +// } +// } + +// #region IRouting Members + +// public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType sigType) +// { +// Debug.Console(2, this, "Making an awesome DM route from {0} to {1} {2}", inputSelector, outputSelector, sigType); + +// var input = Convert.ToUInt32(inputSelector); // Cast can sometimes fail +// var output = Convert.ToUInt32(outputSelector); +// // Check to see if there's an off timer waiting on this and if so, cancel +// var key = new PortNumberType(output, sigType); +// if (input == 0) +// { +// StartOffTimer(key); +// } +// else +// { +// if(RouteOffTimers.ContainsKey(key)) +// { +// Debug.Console(2, this, "{0} cancelling route off due to new source", output); +// RouteOffTimers[key].Stop(); +// RouteOffTimers.Remove(key); +// } +// } + +// Card.DMICard inCard = input == 0 ? null : Chassis.Inputs[input]; + +// // NOTE THAT THESE ARE NOTS - TO CATCH THE AudioVideo TYPE +// if (sigType != eRoutingSignalType.Audio) +// { +// Chassis.VideoEnter.BoolValue = true; +// Chassis.Outputs[output].VideoOut = inCard; +// } + +// if (sigType != eRoutingSignalType.Video) +// { +// Chassis.AudioEnter.BoolValue = true; +// Chassis.Outputs[output].AudioOut = inCard; +// } +// } + +// #endregion + +// } + +// //public struct PortNumberType +// //{ +// // public uint Number { get; private set; } +// // public eRoutingSignalType Type { get; private set; } + +// // public PortNumberType(uint number, eRoutingSignalType type) : this() +// // { +// // Number = number; +// // Type = type; +// // } +// //} +//} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Properties/AssemblyInfo.cs b/essentials-framework/Essentials DM/Essentials_DM/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..d11fa984 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; + +[assembly: AssemblyTitle("Essentials_DM")] +[assembly: AssemblyCompany("PepperDash Technology Corp")] +[assembly: AssemblyProduct("Essentials_DM")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyVersion("1.3.*")] \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/Properties/ControlSystem.cfg b/essentials-framework/Essentials DM/Essentials_DM/Properties/ControlSystem.cfg new file mode 100644 index 00000000..e69de29b diff --git a/essentials-framework/Essentials DM/Essentials_DM/VideoStatusHelpers.cs b/essentials-framework/Essentials DM/Essentials_DM/VideoStatusHelpers.cs new file mode 100644 index 00000000..096b1e36 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/VideoStatusHelpers.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.DM +{ + /// + /// These methods will get the funcs that return values from various video port types... + /// + public class VideoStatusHelper + { + public static VideoStatusFuncsWrapper GetHdmiInputStatusFuncs(HdmiInputPort port) + { + return new VideoStatusFuncsWrapper + { + HdcpActiveFeedbackFunc = () => port.VideoAttributes.HdcpActiveFeedback.BoolValue, + HdcpStateFeedbackFunc = () => port.VideoAttributes.HdcpStateFeedback.ToString(), + VideoResolutionFeedbackFunc = () => port.VideoAttributes.GetVideoResolutionString(), + VideoSyncFeedbackFunc = () => port.SyncDetectedFeedback.BoolValue + }; + } + + public static VideoStatusFuncsWrapper GetHdmiInputStatusFuncs(EndpointHdmiInput port) + { + return new VideoStatusFuncsWrapper + { + VideoResolutionFeedbackFunc = () => port.VideoAttributes.GetVideoResolutionString(), + VideoSyncFeedbackFunc = () => port.SyncDetectedFeedback.BoolValue + }; + } + + public static VideoStatusFuncsWrapper GetVgaInputStatusFuncs(EndpointVgaInput port) + { + return new VideoStatusFuncsWrapper + { + HdcpActiveFeedbackFunc = () => port.VideoAttributes.HdcpActiveFeedback.BoolValue, + HdcpStateFeedbackFunc = () => port.VideoAttributes.HdcpStateFeedback.ToString(), + VideoResolutionFeedbackFunc = () => port.VideoAttributes.GetVideoResolutionString(), + VideoSyncFeedbackFunc = () => port.SyncDetectedFeedback.BoolValue + }; + } + + public static VideoStatusFuncsWrapper GetDmInputStatusFuncs(DMInputPort port) + { + return new VideoStatusFuncsWrapper + { + HdcpActiveFeedbackFunc = () => port.VideoAttributes.HdcpActiveFeedback.BoolValue, + HdcpStateFeedbackFunc = () => port.VideoAttributes.HdcpStateFeedback.ToString(), + VideoResolutionFeedbackFunc = () => port.VideoAttributes.GetVideoResolutionString(), + VideoSyncFeedbackFunc = () => port.SyncDetectedFeedback.BoolValue + }; + } + + public static VideoStatusFuncsWrapper GetDisplayPortInputStatusFuncs(EndpointDisplayPortInput port) + { + return new VideoStatusFuncsWrapper + { + HdcpActiveFeedbackFunc = () => port.VideoAttributes.HdcpActiveFeedback.BoolValue, + HdcpStateFeedbackFunc = () => port.VideoAttributes.HdcpStateFeedback.ToString(), + VideoResolutionFeedbackFunc = () => port.VideoAttributes.GetVideoResolutionString(), + VideoSyncFeedbackFunc = () => port.SyncDetectedFeedback.BoolValue + }; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials DM/Essentials_DM/app.config b/essentials-framework/Essentials DM/Essentials_DM/app.config new file mode 100644 index 00000000..58e92509 --- /dev/null +++ b/essentials-framework/Essentials DM/Essentials_DM/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common.sln b/essentials-framework/Essentials Devices Common/Essentials Devices Common.sln new file mode 100644 index 00000000..84fa1346 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials Devices Common", "Essentials Devices Common\Essentials Devices Common.csproj", "{892B761C-E479-44CE-BD74-243E9214AF13}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {892B761C-E479-44CE-BD74-243E9214AF13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {892B761C-E479-44CE-BD74-243E9214AF13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {892B761C-E479-44CE-BD74-243E9214AF13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {892B761C-E479-44CE-BD74-243E9214AF13}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Audio/GenericAudioOut.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Audio/GenericAudioOut.cs new file mode 100644 index 00000000..d9ba06cf --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Audio/GenericAudioOut.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Common +{ + /// + /// Represents and audio endpoint + /// + public class GenericAudioOut : Device, IRoutingSinkNoSwitching + { + public RoutingInputPort AnyAudioIn { get; private set; } + + public GenericAudioOut(string key, string name) + : base(key, name) + { + AnyAudioIn = new RoutingInputPort(RoutingPortNames.AnyAudioIn, eRoutingSignalType.Audio, + eRoutingPortConnectionType.LineAudio, null, this); + } + + #region IRoutingInputs Members + + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { AnyAudioIn }; } + } + + #endregion + } + + + /// + /// Allows a zone-device's audio control to be attached to the endpoint, for easy routing and + /// control switching. Will also set the zone name on attached devices, like SWAMP or other + /// hardware with names built in. + /// + public class GenericAudioOutWithVolume : GenericAudioOut, IHasVolumeDevice + { + public string VolumeDeviceKey { get; private set; } + public uint VolumeZone { get; private set; } + + public IBasicVolumeControls VolumeDevice + { + get + { + var dev = DeviceManager.GetDeviceForKey(VolumeDeviceKey); + if (dev is IAudioZones) + return (dev as IAudioZones).Zone[VolumeZone]; + else return dev as IBasicVolumeControls; + } + } + + /// + /// Constructor - adds the name to the attached audio device, if appropriate. + /// + /// + /// + /// + /// + public GenericAudioOutWithVolume(string key, string name, string audioDevice, uint zone) + : base(key, name) + { + VolumeDeviceKey = audioDevice; + VolumeZone = zone; + } + + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/AudioCodecBase.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/AudioCodecBase.cs new file mode 100644 index 00000000..15a998a4 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/AudioCodecBase.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.Devices.Common.AudioCodec +{ + public abstract class AudioCodecBase : Device, IHasDialer, IUsageTracking, IAudioCodecInfo + { + + public event EventHandler CallStatusChange; + + public AudioCodecInfo CodecInfo { get; protected set; } + + #region IUsageTracking Members + + /// + /// This object can be added by outside users of this class to provide usage tracking + /// for various services + /// + public UsageTracking UsageTracker { get; set; } + + #endregion + + /// + /// Returns true when any call is not in state Unknown, Disconnecting, Disconnected + /// + public bool IsInCall + { + get + { + bool value; + + if (ActiveCalls != null) + value = ActiveCalls.Any(c => c.IsActiveCall); + else + value = false; + return value; + } + } + + // In most cases only a single call can be active + public List ActiveCalls { get; set; } + + public AudioCodecBase(string key, string name) + : base(key, name) + { + ActiveCalls = new List(); + } + + /// + /// Helper method to fire CallStatusChange event with old and new status + /// + protected void SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus newStatus, CodecActiveCallItem call) + { + call.Status = newStatus; + + OnCallStatusChange(call); + + } + + /// + /// + /// + /// + /// + /// + protected void OnCallStatusChange(CodecActiveCallItem item) + { + var handler = CallStatusChange; + if (handler != null) + handler(this, new CodecCallStatusItemChangeEventArgs(item)); + + if (UsageTracker != null) + { + if (IsInCall && !UsageTracker.UsageTrackingStarted) + UsageTracker.StartDeviceUsage(); + else if (UsageTracker.UsageTrackingStarted && !IsInCall) + UsageTracker.EndDeviceUsage(); + } + } + + #region IHasDialer Members + + public abstract void Dial(string number); + + public abstract void EndCall(CodecActiveCallItem activeCall); + + public abstract void EndAllCalls(); + + public abstract void AcceptCall(CodecActiveCallItem item); + + public abstract void RejectCall(CodecActiveCallItem item); + + public abstract void SendDtmf(string digit); + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/Interfaces/IAudioCodecInfo.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/Interfaces/IAudioCodecInfo.cs new file mode 100644 index 00000000..e853c898 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/Interfaces/IAudioCodecInfo.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.AudioCodec +{ + /// + /// Implements a common set of data about a codec + /// + public interface IAudioCodecInfo + { + AudioCodecInfo CodecInfo { get; } + } + + /// + /// Stores general information about a codec + /// + public abstract class AudioCodecInfo + { + public abstract string PhoneNumber { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/Interfaces/IHasAudioCodec.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/Interfaces/IHasAudioCodec.cs new file mode 100644 index 00000000..e4cb5ee4 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/Interfaces/IHasAudioCodec.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.AudioCodec +{ + /// + /// For rooms that have audio codec + /// + public interface IHasAudioCodec + { + AudioCodecBase AudioCodec { get; } + BoolFeedback InCallFeedback { get; } + + ///// + ///// Make this more specific + ///// + //List ActiveCalls { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/MockAC/MockAC.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/MockAC/MockAC.cs new file mode 100644 index 00000000..e0645624 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/MockAC/MockAC.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.Devices.Common.AudioCodec +{ + public class MockAC : AudioCodecBase + { + public MockAC(string key, string name, MockAcPropertiesConfig props) + : base(key, name) + { + CodecInfo = new MockAudioCodecInfo(); + + CodecInfo.PhoneNumber = props.PhoneNumber; + } + + public override void Dial(string number) + { + if (!IsInCall) + { + Debug.Console(1, this, "Dial: {0}", number); + var call = new CodecActiveCallItem() + { + Name = "Mock Outgoing Call", + Number = number, + Type = eCodecCallType.Audio, + Status = eCodecCallStatus.Connected, + Direction = eCodecCallDirection.Outgoing, + Id = "mockAudioCall-1" + }; + + ActiveCalls.Add(call); + + OnCallStatusChange(call); + } + else + { + Debug.Console(1, this, "Already in call. Cannot dial new call."); + } + } + + public override void EndCall(CodecActiveCallItem call) + { + Debug.Console(1, this, "EndCall"); + ActiveCalls.Remove(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); + } + + public override void EndAllCalls() + { + Debug.Console(1, this, "EndAllCalls"); + for (int i = ActiveCalls.Count - 1; i >= 0; i--) + { + var call = ActiveCalls[i]; + ActiveCalls.Remove(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); + } + } + + public override void AcceptCall(CodecActiveCallItem call) + { + Debug.Console(1, this, "AcceptCall"); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call); + } + + public override void RejectCall(CodecActiveCallItem call) + { + Debug.Console(1, this, "RejectCall"); + ActiveCalls.Remove(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); + } + + public override void SendDtmf(string s) + { + Debug.Console(1, this, "BEEP BOOP SendDTMF: {0}", s); + } + + /// + /// + /// + /// + public void TestIncomingAudioCall(string number) + { + Debug.Console(1, this, "TestIncomingAudioCall from {0}", number); + var call = new CodecActiveCallItem() { Name = number, Id = number, Number = number, Type = eCodecCallType.Audio, Direction = eCodecCallDirection.Incoming }; + ActiveCalls.Add(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Ringing, call); + } + + } + + public class MockAudioCodecInfo : AudioCodecInfo + { + string _phoneNumber; + + public override string PhoneNumber + { + get + { + return _phoneNumber; + } + set + { + _phoneNumber = value; + } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/MockAC/MockAcPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/MockAC/MockAcPropertiesConfig.cs new file mode 100644 index 00000000..7ae375b7 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/AudioCodec/MockAC/MockAcPropertiesConfig.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using Newtonsoft.Json; + +namespace PepperDash.Essentials.Devices.Common.AudioCodec +{ + public class MockAcPropertiesConfig + { + [JsonProperty("phoneNumber")] + public string PhoneNumber { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Cameras/CameraBase.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Cameras/CameraBase.cs new file mode 100644 index 00000000..8a7dcf03 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Cameras/CameraBase.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; +using System.Text.RegularExpressions; +using Crestron.SimplSharp.Reflection; + + +namespace PepperDash.Essentials.Devices.Common.Cameras +{ + public enum eCameraCapabilities + { + None = 0, + Pan = 1, + Tilt = 2, + Zoom = 4, + Focus = 8 + } + + public abstract class CameraBase : Device + { + public bool CanPan + { + get + { + return (Capabilities & eCameraCapabilities.Pan) == eCameraCapabilities.Pan; + } + } + + public bool CanTilt + { + get + { + return (Capabilities & eCameraCapabilities.Tilt) == eCameraCapabilities.Tilt; + } + } + + public bool CanZoom + { + get + { + return (Capabilities & eCameraCapabilities.Zoom) == eCameraCapabilities.Zoom; + } + } + + public bool CanFocus + { + get + { + return (Capabilities & eCameraCapabilities.Focus) == eCameraCapabilities.Focus; + } + } + + // A bitmasked value to indicate the movement capabilites of this camera + protected eCameraCapabilities Capabilities { get; set; } + + public CameraBase(string key, string name) : + base(key, name) { } + } + + public class CameraPropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public ControlPropertiesConfig Control { get; set; } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Cameras/CameraVisca.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Cameras/CameraVisca.cs new file mode 100644 index 00000000..182b560c --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Cameras/CameraVisca.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; +using System.Text.RegularExpressions; +using Crestron.SimplSharp.Reflection; + +namespace PepperDash.Essentials.Devices.Common.Cameras +{ + public class CameraVisca : CameraBase, IHasCameraPtzControl, ICommunicationMonitor + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + public byte PanSpeed = 0x10; + public byte TiltSpeed = 0x10; + private bool IsMoving; + private bool IsZooming; + public bool PowerIsOn { get; private set; } + + byte[] IncomingBuffer = new byte[] { }; + public BoolFeedback PowerIsOnFeedback { get; private set; } + + public CameraVisca(string key, string name, IBasicCommunication comm, CameraPropertiesConfig props) : + base(key, name) + { + + // Default to all capabilties + Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom | eCameraCapabilities.Focus; + + Communication = comm; + var socket = comm as ISocketStatus; + if (socket != null) + { + // This instance uses IP control + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + else + { + // This instance uses RS-232 control + } + PortGather = new CommunicationGather(Communication, "\xFF"); + + + Communication.BytesReceived += new EventHandler(Communication_BytesReceived); + PowerIsOnFeedback = new BoolFeedback(() => { return PowerIsOn; }); + + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 20000, 120000, 300000, "\x81\x09\x04\x00\xFF"); + } + DeviceManager.AddDevice(CommunicationMonitor); + + + } + public override bool CustomActivate() + { + Communication.Connect(); + + + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + + CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + return true; + } + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.Console(2, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); + + if (e.Client.IsConnected) + { + + } + else + { + + } + } + + + void SendBytes(byte[] b) + { + + if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 + Debug.Console(2, this, "Sending:{0}", ComTextHelper.GetEscapedText(b)); + + Communication.SendBytes(b); + } + void Communication_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e) + { + // This is probably not thread-safe buffering + // Append the incoming bytes with whatever is in the buffer + var newBytes = new byte[IncomingBuffer.Length + e.Bytes.Length]; + IncomingBuffer.CopyTo(newBytes, 0); + e.Bytes.CopyTo(newBytes, IncomingBuffer.Length); + if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 + Debug.Console(2, this, "Received:{0}", ComTextHelper.GetEscapedText(newBytes)); + } + + + private void SendPanTiltCommand (byte[] cmd) + { + var temp = new Byte[] { 0x81, 0x01, 0x06, 0x01, PanSpeed, TiltSpeed }; + int length = temp.Length + cmd.Length + 1; + + byte[] sum = new byte[length]; + temp.CopyTo(sum, 0); + cmd.CopyTo(sum, temp.Length); + sum[length - 1] = 0xFF; + SendBytes(sum); + } + + public void PowerOn() + { + + SendBytes(new Byte[] { 0x81, 0x01, 0x04, 0x00, 0x02, 0xFF }); + } + + public void PowerOff() + { + SendBytes(new Byte[] {0x81, 0x01, 0x04, 0x00, 0x03, 0xFF}); + } + public void PanLeft() + { + SendPanTiltCommand(new byte[] {0x01, 0x03}); + IsMoving = true; + } + public void PanRight() + { + SendPanTiltCommand(new byte[] { 0x02, 0x03 }); + IsMoving = true; + } + public void PanStop() + { + Stop(); + } + public void TiltDown() + { + SendPanTiltCommand(new byte[] { 0x03, 0x02 }); + IsMoving = true; + } + public void TiltUp() + { + SendPanTiltCommand(new byte[] { 0x03, 0x01 }); + IsMoving = true; + } + public void TiltStop() + { + Stop(); + } + + private void SendZoomCommand (byte cmd) + { + SendBytes(new byte[] {0x81, 0x01, 0x04, 0x07, cmd, 0xFF} ); + } + public void ZoomIn() + { + SendZoomCommand(0x02); + IsZooming = true; + } + public void ZoomOut() + { + SendZoomCommand(0x03); + IsZooming = true; + } + public void ZoomStop() + { + Stop(); + } + + public void Stop() + { + if (IsZooming) + { + SendZoomCommand(0x00); + IsZooming = false; + } + else + { + SendPanTiltCommand(new byte[] {0x03, 0x03}); + IsMoving = false; + } + } + public void PositionHome() + { + throw new NotImplementedException(); + } + public void RecallPreset(int presetNumber) + { + SendBytes(new byte[] {0x81, 0x01, 0x04, 0x3F, 0x02, (byte)presetNumber, 0xFF} ); + } + public void SavePreset(int presetNumber) + { + SendBytes(new byte[] { 0x81, 0x01, 0x04, 0x3F, 0x01, (byte)presetNumber, 0xFF }); + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/ClassDiagram1.cd b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ClassDiagram1.cd new file mode 100644 index 00000000..8b9d1dd0 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ClassDiagram1.cd @@ -0,0 +1,740 @@ + + + + + + AAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAA= + Audio\GenericAudioOut.cs + + + + + + + AAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAgAAAAQA= + Audio\GenericAudioOut.cs + + + + + + + AAAAAAAAAAAAAAAAAAAIIAAAAAAACABAAAAAAAAAAAA= + Crestron\Gateways\CenRfgwController.cs + + + + + + gUyjAAoIAAAAxgYAABAAgAECABAoAEAQqcCAQHIABAI= + DiscPlayer\IRDiscPlayerBase.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAA= + Factory\DeviceFactory.cs + + + + + + AAgAAAAAAAACAAAAAAAAAAAAAAAgAAAAAAAAAAIAAAA= + Generic\GenericSource.cs + + + + + + + AAwAAAAAAAACAAAAAAACAAAAAQAgAAAAAAAAAAIAAAA= + PC\InRoomPc.cs + + + + + + + AAwAAAAAAAACAAAAAAACAAAAAQAgAAAAAAAAAAIAAAA= + PC\Laptop.cs + + + + + + + AUgjCAqAEAASwgYAACAAoAECABAoAEAwrcCAQHKABAI= + SetTopBox\IRSetTopBoxBase.cs + + + + + + + AAAAAAAEEAAQAAAAAAAAIAAAAAAAAAAAAAAAAACAAAA= + SetTopBox\SetTopBoxPropertiesConfig.cs + + + + + + AAggAAIAAAAARAAAAAAAgAACABAoAEAQqACAQAAABAI= + Streaming\AppleTV.cs + + + + + + + AAggAAIAAAAABAAAAAAAgAACABAoAEAQqACAQAAABAI= + Streaming\Roku.cs + + + + + + + AAACAAAEAAAAAAAAAOAAAAQAAAAAAAAAAQAAAAAAAAA= + Codec\CodecActiveCallItem.cs + + + + + + AAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Codec\CodecActiveCallItem.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAA= + Codec\eCodecCallDirection.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAA= + Codec\eCodecCallStatus.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAA= + Codec\eCodecCallType.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAA= + Codec\eMeetingPrivacy.cs + + + + + + gIAAAAAAAAAAAACCAACAAAAAAAAAAAACAAAAAABAAAA= + Codec\iCodecInfo.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAA= + Codec\iHasCallFavorites.cs + + + + + + AAAAAAAQAAAAAgAAAAAAAAAAAAAAGAAAABAAAAAAACQ= + Codec\iHasCallHistory.cs + + + + + + AAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Codec\iHasDirectory.cs + + + + + + AAAAAAAAAAAIAAAAAAAAAAAABAAAAAIAAIAAQAAAAAA= + Codec\iHasDirectory.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA= + Codec\iHasDirectory.cs + + + + + + BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAIAAAAAAAAA= + Codec\iHasDirectory.cs + + + + + + AAAAAgAAAAAAAEAAAAAAAAAAAAAAAAEIAAAAAAAAAAA= + Codec\iHasDirectory.cs + + + + + + AAAAAIAAAAAAAgAAAEAAAAAAAAAIAAAAAAAAAAAAAAA= + Codec\iHasDirectory.cs + + + + + + AAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Codec\iHasDirectory.cs + + + + + + AAAAAABAAAAAEAAAAAAAgAAEAAAABAAAAAAAIQAAAAA= + Codec\iHasScheduleAwareness.cs + + + + + + AAACAAAAAAACEEgAAAAAAAgQIAAoAQABAAQAAEgAAAA= + Codec\iHasScheduleAwareness.cs + + + + + + AAAAAAAAAAAAAAAAAEAAAABAAAAIAAAAAAAAAAgAAAA= + Codec\iHasScheduleAwareness.cs + + + + + + AAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAQAAAAA= + Codec\iHasScheduleAwareness.cs + + + + + + AAAAAAAAAAAQAAAAAAAAAAAAAAAAAIQAAACAABAAAAA= + VideoCodec\CiscoCodec\CiscoSparkCodecPropertiesConfig.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAA= + VideoCodec\CiscoCodec\CiscoSparkCodecPropertiesConfig.cs + + + + + + hC4gIEAIUAABoQAEgBACAAAdBC1gmsGRICKGdQNBACw= + Display\AvocorVTFDisplay.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAEQAAAAA= + Display\ComTcpDisplayBase.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAA= + Display\DeviceFactory.cs + + + + + + ogSIAAKIAAALCAAEABAQEAgABAAAAMIAAAAAEABAACA= + Display\NecPaSeriesProjector.cs + + + + + + hLQgIShIAABFQQRUAvICDRMQESwSEQKRKACCZQNAESY= + Display\NECPSXMDisplay.cs + + + + + + + hKwwIEAIQAABoUAGgBACAAAEBGxAmYORKCCGZQJAACw= + Display\SamsungMDCDisplay.cs + + + + + + + gACAAIAAAAEAQYCigAAAAAAQAACgCAAAAAAAAAMAAAI= + DSP\BiampTesira\BiampTesiraForteDsp.cs + + + + + + RAAgJACAAAAAAYAAAkIAAAAAIAQCEACJABACRAAAAUQ= + DSP\BiampTesira\BiampTesiraForteDspLevel.cs + + + + + + + BAAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAA= + DSP\BiampTesira\BiampTesiraFortePropertiesConfig.cs + + + + + + AAAAAAAAEAAAAIAAAAAAAAAAAAAAAAAIAAEAAAAAAEQ= + DSP\BiampTesira\BiampTesiraFortePropertiesConfig.cs + + + + + + AAAAIAAAAACAAQQAAAAAQAAAAAAAIAAAABEAAAAAAEA= + DSP\BiampTesira\TesiraForteControlPoint.cs + + + + + + AAAAAAAAAAAEAIAgAAAAAAIAAAAAAAAAAAAAAAAAAAA= + DSP\DspBase.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + DSP\DspBase.cs + + + + + + BAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAQAAAAAQ= + DSP\DspBase.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAA= + DSP\PolycomSoundStructure\SoundStructureBasics.cs + + + + + + gACAEBAAAoAABQBAASAAAAAAAAEgAAACAAACAAMAQAI= + Environment\Lutron\LutronQuantum.cs + + + + + + + AAAAAAAEAAAAAAAAgQAAAAAAAAAAAAQCAAACAAAAAAA= + Environment\Lutron\LutronQuantum.cs + + + + + + gQAABAAAAxAAIACAgAgAgCBAUQAAQAgCIAEQAACBAAA= + Microphone\MicrophonePrivacyController.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAEAAAAAgAIAAAAAAAAAA= + Microphone\MicrophonePrivacyControllerConfig.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAA= + Microphone\MicrophonePrivacyControllerConfig.cs + + + + + + AAAAAAAAAAIAAAAAAAABAABgAAAAAASEABAAAAAAAAA= + Occupancy\EssentialsGlsOccupancySensorBaseController.cs + + + + + + + AAACBAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Occupancy\EssentialsOccupancyAggregator.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAg= + VideoCodec\CiscoCodec\BookingsDataClasses.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + VideoCodec\CiscoCodec\CallHistoryDataClasses.cs + + + + + + EAAAABAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAA= + VideoCodec\CiscoCodec\PhonebookDataClasses.cs + + + + + + BgYAIAACAQaKBACAAwAAUSQAAWYCEACDAAiAQBBCgQU= + VideoCodec\MockVC\MockVC.cs + + + + + + + gKAAAAAAAAAAAACCAACAAAAAAAAAAAACAAAAAABBAAA= + VideoCodec\MockVC\MockVC.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAA= + VideoCodec\MockVC\MockVcPropertiesConfig.cs + + + + + + BiwgAAECAASAAECAgQEDIABAAAYiEBCpCEigQBZCAQU= + VideoCodec\VideoCodecBase.cs + + + + + + + EAiCAAQAAgEAAQQAEAAAAAAAAAEAAAAAAACAAAAAAAM= + VideoCodec\VideoCodecBase.cs + + + + + + + jhQEtAJaASb7kSCwAwtxECSABsf2n1GBJEmAVJFKWTc= + VideoCodec\CiscoCodec\CiscoSparkCodec.cs + + + + + + + AAAAAAAAEAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAA= + VideoCodec\CiscoCodec\CiscoSparkCodec.cs + + + + + + AIAAAAQACAEAAAQAEAAAAAAAAAAgAAAAAAAACAACACE= + VideoCodec\CiscoCodec\CiscoSparkCodec.cs + + + + + + + AAAAQAAAACAAAAABAAAAAAAIAIAAAAAAAQAAgAAAAAA= + VideoCodec\CiscoCodec\HttpApiServer.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + VideoCodec\CiscoCodec\xConfiguration.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + VideoCodec\CiscoCodec\xEvent.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + VideoCodec\CiscoCodec\xStatus.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + VideoCodec\CiscoCodec\xStatusSparkPlus.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Codec\iCodecAudio.cs + + + + + + AAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAA= + Codec\iCodecInfo.cs + + + + + + AAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Codec\iHasCallFavorites.cs + + + + + + AAAAAAAAAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA= + Codec\iHasCallHistory.cs + + + + + + AAAAAAACAAAAAAAAAAAAAAAAAAAAAAAIAEAgABAAAAA= + Codec\iHasContentSharing.cs + + + + + + AAgAAAACAAAAAACAAAAAAAAAAAICAAAAAAAAAAQCAAA= + Codec\iHasDialer.cs + + + + + + AAAAAAAAAAAAgAAAAAAAAAAAAgCAAAAABAAAEAAAAAA= + Codec\iHasDirectory.cs + + + + + + AAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAAAAAAAAAAAA= + Codec\iHasScheduleAwareness.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA= + Display\InputInterfaces.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAA= + Display\InputInterfaces.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAA= + Display\InputInterfaces.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAA= + Display\InputInterfaces.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAA= + Display\InputInterfaces.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAA= + Display\InputInterfaces.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Display\InputInterfaces.cs + + + + + + QAAAAACAAAAAAAAAAAAAAAAAAAAAAAAIAAEAAAAAAAQ= + DSP\DspBase.cs + + + + + + AAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Occupancy\iOccupancyStatusProvider.cs + + + + + + AAAAAAAAAAAgAAAAAAAAAAAAAAAEEgAAAAAAAAAAAAA= + VideoCodec\Interfaces\IHasCodecLayouts.cs + + + + + + ABAAEAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAABAAAAI= + VideoCodec\Interfaces\IHasCodecSelfview.cs + + + + + + AAAAAAAgAAAAAAAAAAAAAIAAAEAAAAAAAAAAAAAAAAA= + Crestron\Gateways\CenRfgwController.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAgQAAAAAAAAAAAABA= + Codec\eCodecCallDirection.cs + + + + + + CAgAAABAAAAAQAACAAABAAAAAAAAAAABCAAAAiAAQBA= + Codec\eCodecCallStatus.cs + + + + + + AAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAoBg= + Codec\eCodecCallType.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAQBA= + Codec\eMeetingPrivacy.cs + + + + + + AAAAAAABAAAAAIAAAAAAAAAAAAAAAAAAAEAAAAAAABA= + Codec\iHasCallHistory.cs + + + + + + BAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAIAAAAAAABg= + Codec\iHasDirectory.cs + + + + + + AAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAABg= + Codec\iHasDirectory.cs + + + + + + BAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAIACg= + Codec\iHasScheduleAwareness.cs + + + + + + AACAACAAABAAgIAAgAAQAAAAAAAAIAAAIAAAACAEAAg= + Environment\Lutron\LutronQuantum.cs + + + + + + AAAAAAAAAAAACAQAAAAABAAEAAAAAAAAgAAAAAAAAAA= + VideoCodec\CiscoCodec\CiscoSparkCodec.cs + + + + \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/CodecActiveCallItem.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/CodecActiveCallItem.cs new file mode 100644 index 00000000..75c4fc1a --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/CodecActiveCallItem.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace PepperDash.Essentials.Devices.Common.Codec + +{ + public class CodecActiveCallItem + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("number")] + public string Number { get; set; } + + [JsonProperty("type")] + [JsonConverter(typeof(StringEnumConverter))] + public eCodecCallType Type { get; set; } + + [JsonProperty("status")] + [JsonConverter(typeof(StringEnumConverter))] + public eCodecCallStatus Status { get; set; } + + [JsonProperty("direction")] + [JsonConverter(typeof(StringEnumConverter))] + public eCodecCallDirection Direction { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + + //public object CallMetaData { get; set; } + + /// + /// Returns true when this call is any status other than + /// Unknown, Disconnected, Disconnecting + /// + [JsonProperty("isActiveCall")] + public bool IsActiveCall + { + get + { + return !(Status == eCodecCallStatus.Disconnected + || Status == eCodecCallStatus.Disconnecting + || Status == eCodecCallStatus.Idle + || Status == eCodecCallStatus.Unknown); + } + } + } + + /// + /// + /// + public class CodecCallStatusItemChangeEventArgs : EventArgs + { + public CodecActiveCallItem CallItem { get; private set; } + + public CodecCallStatusItemChangeEventArgs(CodecActiveCallItem item) + { + CallItem = item; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/CodecActiveCallItem.cs.orig b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/CodecActiveCallItem.cs.orig new file mode 100644 index 00000000..46746037 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/CodecActiveCallItem.cs.orig @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.Codec + +{ + public class CodecActiveCallItem + { + public string Name { get; set; } + + public string Number { get; set; } + +<<<<<<< HEAD:Essentials Devices Common/Essentials Devices Common/Codec/CodecActiveCallItem.cs + public eCodecCallType Type { get; set; } + + public eCodecCallStatus Status { get; set; } + + public string Id { get; set; } +======= + public eCodecCallType Type { get; set; } + + public eCodecCallStatus Status { get; set; } + + public string Id { get; set; } + + public object CallMetaData { get; set; } +>>>>>>> origin/feature/cisco-spark-2:Essentials Devices Common/Essentials Devices Common/VideoCodec/CodecActiveCallItem.cs + } + + public enum eCodecCallType + { + Unknown = 0, Audio, Video +<<<<<<< HEAD:Essentials Devices Common/Essentials Devices Common/Codec/CodecActiveCallItem.cs + } + + public enum eCodecCallStatus + { + Unknown = 0, Dialing, Connected, Incoming, OnHold, Disconnected +======= + } + + public enum eCodecCallStatus + { + Unknown = 0, Dialing, Connected, Incoming, OnHold, Disconnected +>>>>>>> origin/feature/cisco-spark-2:Essentials Devices Common/Essentials Devices Common/VideoCodec/CodecActiveCallItem.cs + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallDirection.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallDirection.cs new file mode 100644 index 00000000..a5e118df --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallDirection.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.Codec + +{ + public enum eCodecCallDirection + { + Unknown = 0, Incoming, Outgoing + } + + public class CodecCallDirection + { + /// + /// Takes the Cisco call type and converts to the matching enum + /// + /// + /// + public static eCodecCallDirection ConvertToDirectionEnum(string s) + { + switch (s.ToLower()) + { + case "incoming": + { + return eCodecCallDirection.Incoming; + } + case "outgoing": + { + return eCodecCallDirection.Outgoing; + } + default: + return eCodecCallDirection.Unknown; + } + + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallStatus.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallStatus.cs new file mode 100644 index 00000000..610d928b --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallStatus.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.Codec +{ + public enum eCodecCallStatus + { + Unknown = 0, + Connected, + Connecting, + Dialing, + Disconnected, + Disconnecting, + EarlyMedia, + Idle, + OnHold, + Ringing, + Preserved, + RemotePreserved, + } + + + public class CodecCallStatus + { + + /// + /// Takes the Cisco call type and converts to the matching enum + /// + /// + /// + public static eCodecCallStatus ConvertToStatusEnum(string s) + { + switch (s) + { + case "Connected": + { + return eCodecCallStatus.Connected; + } + case "Connecting": + { + return eCodecCallStatus.Connecting; + } + case "Dialling": + { + return eCodecCallStatus.Dialing; + } + case "Disconnected": + { + return eCodecCallStatus.Disconnected; + } + case "Disconnecting": + { + return eCodecCallStatus.Disconnecting; + } + case "EarlyMedia": + { + return eCodecCallStatus.EarlyMedia; + } + case "Idle": + { + return eCodecCallStatus.Idle; + } + case "OnHold": + { + return eCodecCallStatus.OnHold; + } + case "Ringing": + { + return eCodecCallStatus.Ringing; + } + case "Preserved": + { + return eCodecCallStatus.Preserved; + } + case "RemotePreserved": + { + return eCodecCallStatus.RemotePreserved; + } + default: + return eCodecCallStatus.Unknown; + } + + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallType.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallType.cs new file mode 100644 index 00000000..dbab015b --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eCodecCallType.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.Codec + +{ + public enum eCodecCallType + { + Unknown = 0, + Audio, + Video, + AudioCanEscalate, + ForwardAllCall + } + + public class CodecCallType + { + + /// + /// Takes the Cisco call type and converts to the matching enum + /// + /// + /// + public static eCodecCallType ConvertToTypeEnum(string s) + { + switch (s) + { + case "Audio": + { + return eCodecCallType.Audio; + } + case "Video": + { + return eCodecCallType.Video; + } + case "AudioCanEscalate": + { + return eCodecCallType.AudioCanEscalate; + } + case "ForwardAllCall": + { + return eCodecCallType.ForwardAllCall; + } + default: + return eCodecCallType.Unknown; + } + + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eMeetingPrivacy.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eMeetingPrivacy.cs new file mode 100644 index 00000000..f163a864 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/eMeetingPrivacy.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.Codec +{ + public enum eMeetingPrivacy + { + Unknown = 0, + Public, + Private + } + + public class CodecCallPrivacy + { + /// + /// Takes the Cisco privacy type and converts to the matching enum + /// + /// + /// + public static eMeetingPrivacy ConvertToDirectionEnum(string s) + { + switch (s.ToLower()) + { + case "public": + { + return eMeetingPrivacy.Public; + } + case "private": + { + return eMeetingPrivacy.Private; + } + default: + return eMeetingPrivacy.Unknown; + } + + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iCodecAudio.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iCodecAudio.cs new file mode 100644 index 00000000..d61f7d26 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iCodecAudio.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.Codec +{ + /// + /// Defines minimum volume controls for a codec device with dialing capabilities + /// + public interface ICodecAudio : IBasicVolumeWithFeedback, IPrivacy + { + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasCallFavorites.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasCallFavorites.cs new file mode 100644 index 00000000..50c1b2c9 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasCallFavorites.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.Codec +{ + public interface IHasCallFavorites + { + CodecCallFavorites CallFavorites { get; } + } + + /// + /// Represents favorites entries for a codec device + /// + public class CodecCallFavorites + { + public List Favorites { get; set; } + + public CodecCallFavorites() + { + Favorites = new List(); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasCallHistory.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasCallHistory.cs new file mode 100644 index 00000000..256938d1 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasCallHistory.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace PepperDash.Essentials.Devices.Common.Codec +{ + public interface IHasCallHistory + { + CodecCallHistory CallHistory { get; } + + void RemoveCallHistoryEntry(CodecCallHistory.CallHistoryEntry entry); + } + + public enum eCodecOccurrenceType + { + Unknown = 0, + Placed, + Received, + NoAnswer + } + + /// + /// Represents the recent call history for a codec device + /// + public class CodecCallHistory + { + public event EventHandler RecentCallsListHasChanged; + + public List RecentCalls { get; private set; } + + /// + /// Item that gets added to the list when there are no recent calls in history + /// + CallHistoryEntry ListEmptyEntry; + + public CodecCallHistory() + { + ListEmptyEntry = new CallHistoryEntry() { Name = "No Recent Calls" }; + + RecentCalls = new List(); + + RecentCalls.Add(ListEmptyEntry); + } + + void OnRecentCallsListChange() + { + var handler = RecentCallsListHasChanged; + if (handler != null) + { + handler(this, new EventArgs()); + } + } + + public void RemoveEntry(CallHistoryEntry entry) + { + RecentCalls.Remove(entry); + OnRecentCallsListChange(); + } + + /// + /// Generic call history entry, not device specific + /// + public class CallHistoryEntry : CodecActiveCallItem + { + [JsonConverter(typeof(IsoDateTimeConverter))] + [JsonProperty("startTime")] + public DateTime StartTime { get; set; } + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("occurrenceType")] + public eCodecOccurrenceType OccurrenceType { get; set; } + [JsonProperty("occurrenceHistoryId")] + public string OccurrenceHistoryId { get; set; } + } + + /// + /// Converts a list of call history entries returned by a Cisco codec to the generic list type + /// + /// + /// + public void ConvertCiscoCallHistoryToGeneric(List entries) + { + var genericEntries = new List(); + + foreach (CiscoCallHistory.Entry entry in entries) + { + + genericEntries.Add(new CallHistoryEntry() + { + Name = entry.DisplayName.Value, + Number = entry.CallbackNumber.Value, + StartTime = entry.LastOccurrenceStartTime.Value, + OccurrenceHistoryId = entry.LastOccurrenceHistoryId.Value, + OccurrenceType = ConvertToOccurenceTypeEnum(entry.OccurrenceType.Value) + }); + } + + // Check if list is empty and if so, add an item to display No Recent Calls + if(genericEntries.Count == 0) + genericEntries.Add(ListEmptyEntry); + + RecentCalls = genericEntries; + OnRecentCallsListChange(); + } + + /// + /// Takes the Cisco occurence type and converts it to the matching enum + /// + /// + /// Requirements for a device that has dialing capabilities + /// + public interface IHasDialer + { + // Add requirements for Dialer functionality + + event EventHandler CallStatusChange; + + void Dial(string number); + void EndCall(CodecActiveCallItem activeCall); + void EndAllCalls(); + void AcceptCall(CodecActiveCallItem item); + void RejectCall(CodecActiveCallItem item); + void SendDtmf(string digit); + + bool IsInCall { get; } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasDirectory.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasDirectory.cs new file mode 100644 index 00000000..3a984ab2 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasDirectory.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.Devices.Common.Codec +{ + /// + /// Defines the API for codecs with a directory + /// + public interface IHasDirectory + { + event EventHandler DirectoryResultReturned; + + CodecDirectory DirectoryRoot { get; } + + CodecDirectory CurrentDirectoryResult { get; } + + CodecPhonebookSyncState PhonebookSyncState { get; } + + void SearchDirectory(string searchString); + + void GetDirectoryFolderContents(string folderId); + + void SetCurrentDirectoryToRoot(); + + void GetDirectoryParentFolderContents(); + + BoolFeedback CurrentDirectoryResultIsNotDirectoryRoot { get; } + + /// + /// Tracks the directory browse history when browsing beyond the root directory + /// + List DirectoryBrowseHistory { get; } + } + + /// + /// + /// + public class DirectoryEventArgs : EventArgs + { + public CodecDirectory Directory { get; set; } + public bool DirectoryIsOnRoot { get; set; } + } + + /// + /// Represents a codec directory + /// + public class CodecDirectory + { + /// + /// Represents the contents of the directory + /// + [JsonProperty("directoryResults")] + public List CurrentDirectoryResults { get; private set; } + + /// + /// Used to store the ID of the current folder for CurrentDirectoryResults + /// + [JsonProperty("resultsFolderId")] + public string ResultsFolderId { get; set; } + + public CodecDirectory() + { + CurrentDirectoryResults = new List(); + } + + /// + /// Adds folders to the directory + /// + /// + public void AddFoldersToDirectory(List folders) + { + if(folders != null) + CurrentDirectoryResults.AddRange(folders); + + SortDirectory(); + } + + /// + /// Adds contacts to the directory + /// + /// + public void AddContactsToDirectory(List contacts) + { + if(contacts != null) + CurrentDirectoryResults.AddRange(contacts); + + SortDirectory(); + } + + /// + /// Sorts the DirectoryResults list to display all folders alphabetically, then all contacts alphabetically + /// + private void SortDirectory() + { + var sortedFolders = new List(); + + sortedFolders.AddRange(CurrentDirectoryResults.Where(f => f is DirectoryFolder)); + + sortedFolders.OrderBy(f => f.Name); + + var sortedContacts = new List(); + + sortedContacts.AddRange(CurrentDirectoryResults.Where(c => c is DirectoryContact)); + + sortedFolders.OrderBy(c => c.Name); + + CurrentDirectoryResults.Clear(); + + CurrentDirectoryResults.AddRange(sortedFolders); + + CurrentDirectoryResults.AddRange(sortedContacts); + } + + } + + /// + /// Used to decorate a contact to indicate it can be invided to a meeting + /// + public interface IInvitableContact + { + + } + + /// + /// Represents an item in the directory + /// + public class DirectoryItem : ICloneable + { + public object Clone() + { + return this.MemberwiseClone(); + } + + [JsonProperty("folderId")] + public string FolderId { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + } + + /// + /// Represents a folder type DirectoryItem + /// + public class DirectoryFolder : DirectoryItem + { + [JsonProperty("contacts")] + public List Contacts { get; set; } + + [JsonProperty("parentFolderId")] + public string ParentFolderId { get; set; } + + public DirectoryFolder() + { + Contacts = new List(); + } + } + + /// + /// Represents a contact type DirectoryItem + /// + public class DirectoryContact : DirectoryItem + { + [JsonProperty("contactId")] + public string ContactId { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("contactMethods")] + public List ContactMethods { get; set; } + + public DirectoryContact() + { + ContactMethods = new List(); + } + } + + /// + /// Represents a method of contact for a contact + /// + public class ContactMethod + { + [JsonProperty("contactMethodId")] + public string ContactMethodId { get; set; } + + [JsonProperty("number")] + public string Number { get; set; } + + [JsonProperty("device")] + [JsonConverter(typeof(StringEnumConverter))] + public eContactMethodDevice Device { get; set; } + + [JsonProperty("callType")] + [JsonConverter(typeof(StringEnumConverter))] + public eContactMethodCallType CallType { get; set; } + } + + /// + /// + /// + public enum eContactMethodDevice + { + Unknown = 0, + Mobile, + Other, + Telephone, + Video + } + + /// + /// + /// + public enum eContactMethodCallType + { + Unknown = 0, + Audio, + Video + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasScheduleAwareness.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasScheduleAwareness.cs new file mode 100644 index 00000000..5a006388 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Codec/iHasScheduleAwareness.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.Codec +{ + public enum eMeetingEventChangeType + { + Unkown = 0, + MeetingStartWarning, + MeetingStart, + MeetingEndWarning, + MeetingEnd + } + + public interface IHasScheduleAwareness + { + CodecScheduleAwareness CodecSchedule { get; } + + void GetSchedule(); + } + + public class CodecScheduleAwareness + { + List _Meetings; + + public event EventHandler MeetingEventChange; + + public event EventHandler MeetingsListHasChanged; + + /// + /// Setter triggers MeetingsListHasChanged event + /// + public List Meetings + { + get + { + return _Meetings; + } + set + { + _Meetings = value; + + var handler = MeetingsListHasChanged; + if (handler != null) + { + handler(this, new EventArgs()); + } + } + } + + private CTimer ScheduleChecker; + + public CodecScheduleAwareness() + { + Meetings = new List(); + + ScheduleChecker = new CTimer(CheckSchedule, null, 1000, 1000); + } + + private void OnMeetingChange(eMeetingEventChangeType changeType, Meeting meeting) + { + var handler = MeetingEventChange; + if (handler != null) + { + handler(this, new MeetingEventArgs() { ChangeType = changeType, Meeting = meeting }); + } + } + + private void CheckSchedule(object o) + { + // Iterate the meeting list and check if any meeting need to do anythingk + + foreach (Meeting m in Meetings) + { + eMeetingEventChangeType changeType = eMeetingEventChangeType.Unkown; + + if (m.TimeToMeetingStart.TotalMinutes <= m.MeetingWarningMinutes.TotalMinutes) // Meeting is about to start + changeType = eMeetingEventChangeType.MeetingStartWarning; + else if (m.TimeToMeetingStart.TotalMinutes == 0) // Meeting Start + changeType = eMeetingEventChangeType.MeetingStart; + else if (m.TimeToMeetingEnd.TotalMinutes <= m.MeetingWarningMinutes.TotalMinutes) // Meeting is about to end + changeType = eMeetingEventChangeType.MeetingEndWarning; + else if (m.TimeToMeetingEnd.TotalMinutes == 0) // Meeting has ended + changeType = eMeetingEventChangeType.MeetingEnd; + + if (changeType != eMeetingEventChangeType.Unkown) + OnMeetingChange(changeType, m); + } + + + } + } + + /// + /// Generic class to represent a meeting (Cisco or Polycom OBTP or Fusion) + /// + public class Meeting + { + public TimeSpan MeetingWarningMinutes = TimeSpan.FromMinutes(5); + + public string Id { get; set; } + public string Organizer { get; set; } + public string Title { get; set; } + public string Agenda { get; set; } + public TimeSpan TimeToMeetingStart + { + get + { + return StartTime - DateTime.Now; + } + } + public TimeSpan TimeToMeetingEnd + { + get + { + return EndTime - DateTime.Now; + } + } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public TimeSpan Duration + { + get + { + return EndTime - StartTime; + } + } + public eMeetingPrivacy Privacy { get; set; } + public bool Joinable + { + get + { + return StartTime.AddMinutes(-5) <= DateTime.Now + && DateTime.Now <= EndTime; //.AddMinutes(-5); + } + } + //public string ConferenceNumberToDial { get; set; } + public string ConferencePassword { get; set; } + public bool IsOneButtonToPushMeeting { get; set; } + + public List Calls { get; private set; } + + public Meeting() + { + Calls = new List(); + } + } + + public class Call + { + public string Number { get; set; } + public string Protocol { get; set; } + public string CallRate { get; set; } + public string CallType { get; set; } + } + + public class MeetingEventArgs : EventArgs + { + public eMeetingEventChangeType ChangeType { get; set; } + public Meeting Meeting { get; set; } + } + +} diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Crestron/Gateways/CenRfgwController.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Crestron/Gateways/CenRfgwController.cs new file mode 100644 index 00000000..ee37674a --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Crestron/Gateways/CenRfgwController.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.Gateways; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common +{ + public class CenRfgwController : CrestronGenericBaseDevice + { + public GatewayBase Gateway { get { return Hardware as GatewayBase; } } + + /// + /// Constructor for the on-board gateway + /// + /// + /// + public CenRfgwController(string key, string name, GatewayBase gateway) : + base(key, name, gateway) + { + } + + public static CenRfgwController GetNewExGatewayController(string key, string name, string id, string gatewayType) + { + uint uid; + eExGatewayType type; + try + { + uid = Convert.ToUInt32(id, 16); + type = (eExGatewayType)Enum.Parse(typeof(eExGatewayType), gatewayType, true); + var cs = Global.ControlSystem; + + GatewayBase gw = null; + switch (type) + { + case eExGatewayType.Ethernet: + gw = new CenRfgwEx(uid, cs); + break; + case eExGatewayType.EthernetShared: + gw = new CenRfgwExEthernetSharable(uid, cs); + break; + case eExGatewayType.Cresnet: + gw = new CenRfgwExCresnet(uid, cs); + break; + } + return new CenRfgwController(key, name, gw); + } + catch (Exception) + { + Debug.Console(0, "ERROR: Cannot create EX Gateway, id {0}, type {1}", id, gatewayType); + return null; + } + } + public static CenRfgwController GetNewErGatewayController(string key, string name, string id, string gatewayType) + { + uint uid; + eExGatewayType type; + try + { + uid = Convert.ToUInt32(id, 16); + type = (eExGatewayType)Enum.Parse(typeof(eExGatewayType), gatewayType, true); + var cs = Global.ControlSystem; + + GatewayBase gw = null; + switch (type) + { + case eExGatewayType.Ethernet: + gw = new CenErfgwPoe(uid, cs); + break; + case eExGatewayType.EthernetShared: + gw = new CenErfgwPoeEthernetSharable(uid, cs); + break; + case eExGatewayType.Cresnet: + gw = new CenErfgwPoeCresnet(uid, cs); + break; + } + return new CenRfgwController(key, name, gw); + } + catch (Exception) + { + Debug.Console(0, "ERROR: Cannot create EX Gateway, id {0}, type {1}", id, gatewayType); + return null; + } + } + + + /// + /// Gets the actual Crestron EX gateway for a given key. "processor" or the key of + /// a CenRfgwExController in DeviceManager + /// + /// + /// Either processor GW or Gateway property of CenRfgwExController + public static GatewayBase GetExGatewayBaseForKey(string key) + { + key = key.ToLower(); + if (key == "processor" && Global.ControlSystem.SupportsInternalRFGateway) + return Global.ControlSystem.ControllerRFGatewayDevice; + var gwCont = DeviceManager.GetDeviceForKey(key) as CenRfgwController; + if (gwCont != null) + return gwCont.Gateway; + + return null; + } + } + + public enum eExGatewayType + { + Ethernet, EthernetShared, Cresnet + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraForteDsp.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraForteDsp.cs new file mode 100644 index 00000000..27a29145 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraForteDsp.cs @@ -0,0 +1,376 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System.Text.RegularExpressions; + + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + + // QUESTIONS: + // + // When subscribing, just use the Instance ID for Custom Name? + + // Verbose on subscriptions? + + // Example subscription feedback responses + // ! "publishToken":"name" "value":-77.0 + // ! "myLevelName" -77 + + public class BiampTesiraForteDsp : DspBase + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + + new public Dictionary LevelControlPoints { get; private set; } + + public bool isSubscribed; + + private CTimer SubscriptionTimer; + + CrestronQueue CommandQueue; + + bool CommandQueueInProgress = false; + + //new public Dictionary DialerControlPoints { get; private set; } + + //new public Dictionary SwitcherControlPoints { get; private set; } + + /// + /// Shows received lines as hex + /// + public bool ShowHexResponse { get; set; } + + public BiampTesiraForteDsp(string key, string name, IBasicCommunication comm, BiampTesiraFortePropertiesConfig props) : + base(key, name) + { + CommandQueue = new CrestronQueue(100); + + Communication = comm; + var socket = comm as ISocketStatus; + if (socket != null) + { + // This instance uses IP control + + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + else + { + // This instance uses RS-232 control + } + PortGather = new CommunicationGather(Communication, "\x0d\x0a"); + PortGather.LineReceived += this.Port_LineReceived; + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { +//#warning Need to deal with this poll string + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 120000, 120000, 300000, "SESSION get aliases\x0d\x0a"); + } + + LevelControlPoints = new Dictionary(); + + foreach (KeyValuePair block in props.LevelControlBlocks) + { + this.LevelControlPoints.Add(block.Key, new TesiraForteLevelControl(block.Key, block.Value, this)); + } + + } + + public override bool CustomActivate() + { + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + CrestronConsole.AddNewConsoleCommand(SendLine, "send" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + return true; + } + + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.Console(2, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); + + if (e.Client.IsConnected) + { + // Tasks on connect + } + else + { + // Cleanup items from this session + + if (SubscriptionTimer != null) + { + SubscriptionTimer.Stop(); + SubscriptionTimer = null; + } + + isSubscribed = false; + CommandQueue.Clear(); + CommandQueueInProgress = false; + } + } + + /// + /// Initiates the subscription process to the DSP + /// + void SubscribeToAttributes() + { + SendLine("SESSION set verbose true"); + + foreach (KeyValuePair level in LevelControlPoints) + { + level.Value.Subscribe(); + } + + if (!CommandQueueInProgress) + SendNextQueuedCommand(); + + ResetSubscriptionTimer(); + } + + /// + /// Resets or Sets the subscription timer + /// + void ResetSubscriptionTimer() + { + isSubscribed = true; + + if (SubscriptionTimer != null) + { + SubscriptionTimer = new CTimer(o => SubscribeToAttributes(), 30000); + SubscriptionTimer.Reset(); + + } + } + + /// + /// Handles a response message from the DSP + /// + /// + /// + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + if (Debug.Level == 2) + Debug.Console(2, this, "RX: '{0}'", + ShowHexResponse ? ComTextHelper.GetEscapedText(args.Text) : args.Text); + + Debug.Console(1, this, "RX: '{0}'", args.Text); + + try + { + if (args.Text.IndexOf("Welcome to the Tesira Text Protocol Server...") > -1) + { + // Indicates a new TTP session + + SubscribeToAttributes(); + } + else if (args.Text.IndexOf("publishToken") > -1) + { + // response is from a subscribed attribute + + string pattern = "! \"publishToken\":[\"](.*)[\"] \"value\":(.*)"; + + Match match = Regex.Match(args.Text, pattern); + + if (match.Success) + { + + string key; + + string customName; + + string value; + + customName = match.Groups[1].Value; + + // Finds the key (everything before the '~' character + key = customName.Substring(0, customName.IndexOf("~", 0) - 1); + + value = match.Groups[2].Value; + + foreach (KeyValuePair controlPoint in LevelControlPoints) + { + if (customName == controlPoint.Value.LevelCustomName || customName == controlPoint.Value.MuteCustomName) + { + controlPoint.Value.ParseSubscriptionMessage(customName, value); + return; + } + + } + } + + /// same for dialers + /// same for switchers + + } + else if (args.Text.IndexOf("+OK") > -1) + { + if (args.Text == "+OK" || args.Text.IndexOf("list\":") > -1 ) // Check for a simple "+OK" only 'ack' repsonse or a list response and ignore + return; + + // response is not from a subscribed attribute. From a get/set/toggle/increment/decrement command + + if (!CommandQueue.IsEmpty) + { + if (CommandQueue.Peek() is QueuedCommand) + { + // Expected response belongs to a child class + QueuedCommand tempCommand = (QueuedCommand)CommandQueue.TryToDequeue(); + //Debug.Console(1, this, "Command Dequeued. CommandQueue Size: {0}", CommandQueue.Count); + + tempCommand.ControlPoint.ParseGetMessage(tempCommand.AttributeCode, args.Text); + } + else + { + // Expected response belongs to this class + string temp = (string)CommandQueue.TryToDequeue(); + //Debug.Console(1, this, "Command Dequeued. CommandQueue Size: {0}", CommandQueue.Count); + + } + + if (CommandQueue.IsEmpty) + CommandQueueInProgress = false; + else + SendNextQueuedCommand(); + + } + + + } + else if (args.Text.IndexOf("-ERR") > -1) + { + // Error response + + switch (args.Text) + { + case "-ERR ALREADY_SUBSCRIBED": + { + ResetSubscriptionTimer(); + break; + } + default: + { + Debug.Console(0, this, "Error From DSP: '{0}'", args.Text); + break; + } + } + + } + } + catch (Exception e) + { + if (Debug.Level == 2) + Debug.Console(2, this, "Error parsing response: '{0}'\n{1}", args.Text, e); + } + + } + + /// + /// Sends a command to the DSP (with delimiter appended) + /// + /// Command to send + public void SendLine(string s) + { + Debug.Console(1, this, "TX: '{0}'", s); + Communication.SendText(s + "\x0a"); + } + + /// + /// Adds a command from a child module to the queue + /// + /// Command object from child module + public void EnqueueCommand(QueuedCommand commandToEnqueue) + { + CommandQueue.Enqueue(commandToEnqueue); + //Debug.Console(1, this, "Command (QueuedCommand) Enqueued '{0}'. CommandQueue has '{1}' Elements.", commandToEnqueue.Command, CommandQueue.Count); + + if(!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Adds a raw string command to the queue + /// + /// + public void EnqueueCommand(string command) + { + CommandQueue.Enqueue(command); + //Debug.Console(1, this, "Command (string) Enqueued '{0}'. CommandQueue has '{1}' Elements.", command, CommandQueue.Count); + + if (!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Sends the next queued command to the DSP + /// + void SendNextQueuedCommand() + { + //Debug.Console(2, this, "Attempting to send next queued command. CommandQueueInProgress: {0} Communication isConnected: {1}", CommandQueueInProgress, Communication.IsConnected); + + //if (CommandQueue.IsEmpty) + // CommandQueueInProgress = false; + + //Debug.Console(1, this, "CommandQueue has {0} Elements:\n", CommandQueue.Count); + + //foreach (object o in CommandQueue) + //{ + // if (o is string) + // Debug.Console(1, this, "{0}", o); + // else if(o is QueuedCommand) + // { + // var item = (QueuedCommand)o; + // Debug.Console(1, this, "{0}", item.Command); + // } + //} + + //Debug.Console(1, this, "End of CommandQueue"); + + if (Communication.IsConnected && !CommandQueue.IsEmpty) + { + CommandQueueInProgress = true; + + if (CommandQueue.Peek() is QueuedCommand) + { + QueuedCommand nextCommand = new QueuedCommand(); + + nextCommand = (QueuedCommand)CommandQueue.Peek(); + + SendLine(nextCommand.Command); + } + else + { + string nextCommand = (string)CommandQueue.Peek(); + + SendLine(nextCommand); + } + } + + } + + /// + /// Sends a command to execute a preset + /// + /// Preset Name + public override void RunPreset(string name) + { + SendLine(string.Format("DEVICE recallPreset {0}", name)); + } + + public class QueuedCommand + { + public string Command { get; set; } + public string AttributeCode { get; set; } + public TesiraForteControlPoint ControlPoint { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraForteDspLevel.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraForteDspLevel.cs new file mode 100644 index 00000000..d0f83115 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraForteDspLevel.cs @@ -0,0 +1,366 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System.Text.RegularExpressions; + + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + public class TesiraForteLevelControl : TesiraForteControlPoint, IBiampTesiraDspLevelControl, IKeyed + { + bool _IsMuted; + ushort _VolumeLevel; + + public BoolFeedback MuteFeedback { get; private set; } + + public IntFeedback VolumeLevelFeedback { get; private set; } + + + public bool Enabled { get; set; } + public string ControlPointTag { get { return base.InstanceTag; } } + + /// + /// Used for to identify level subscription values + /// + public string LevelCustomName { get; private set; } + + /// + /// Used for to identify mute subscription values + /// + public string MuteCustomName { get; private set; } + + /// + /// Minimum fader level + /// + double MinLevel; + + /// + /// Maximum fader level + /// + double MaxLevel; + + /// + /// Checks if a valid subscription string has been recieved for all subscriptions + /// + public bool IsSubsribed + { + get + { + bool isSubscribed = false; + + if (HasMute && MuteIsSubscribed) + isSubscribed = true; + + if (HasLevel && LevelIsSubscribed) + isSubscribed = true; + + return isSubscribed; + } + } + + public bool AutomaticUnmuteOnVolumeUp { get; private set; } + + public bool HasMute { get; private set; } + + public bool HasLevel { get; private set; } + + bool MuteIsSubscribed; + + bool LevelIsSubscribed; + + //public TesiraForteLevelControl(string label, string id, int index1, int index2, bool hasMute, bool hasLevel, BiampTesiraForteDsp parent) + // : base(id, index1, index2, parent) + //{ + // Initialize(label, hasMute, hasLevel); + //} + + public TesiraForteLevelControl(string key, BiampTesiraForteLevelControlBlockConfig config, BiampTesiraForteDsp parent) + : base(config.InstanceTag, config.Index1, config.Index2, parent) + { + Initialize(key, config.Label, config.HasMute, config.HasLevel); + } + + + /// + /// Initializes this attribute based on config values and generates subscriptions commands and adds commands to the parent's queue. + /// + public void Initialize(string key, string label, bool hasMute, bool hasLevel) + { + Key = string.Format("{0}--{1}", Parent.Key, key); + + DeviceManager.AddDevice(this); + + Debug.Console(2, this, "Adding LevelControl '{0}'", Key); + + this.IsSubscribed = false; + + MuteFeedback = new BoolFeedback(() => _IsMuted); + + VolumeLevelFeedback = new IntFeedback(() => _VolumeLevel); + + HasMute = hasMute; + HasLevel = hasLevel; + } + + public void Subscribe() + { + // Do subscriptions and blah blah + + // Subscribe to mute + if (this.HasMute) + { + MuteCustomName = string.Format("{0}~mute{1}", this.InstanceTag, this.Index1); + + SendSubscriptionCommand(MuteCustomName, "mute", 500); + } + + // Subscribe to level + if (this.HasLevel) + { + LevelCustomName = string.Format("{0}~level{1}", this.InstanceTag, this.Index1); + + SendSubscriptionCommand(LevelCustomName, "level", 250); + + SendFullCommand("get", "minLevel", null); + + SendFullCommand("get", "maxLevel", null); + } + } + + + /// + /// Parses the response from the DspBase + /// + /// + /// + public void ParseSubscriptionMessage(string customName, string value) + { + + // Check for valid subscription response + + if (this.HasMute && customName == MuteCustomName) + { + //if (value.IndexOf("+OK") > -1) + //{ + // int pointer = value.IndexOf(" +OK"); + + // MuteIsSubscribed = true; + + // // Removes the +OK + // value = value.Substring(0, value.Length - (value.Length - (pointer - 1))); + //} + + if (value.IndexOf("true") > -1) + { + _IsMuted = true; + MuteIsSubscribed = true; + + } + else if (value.IndexOf("false") > -1) + { + _IsMuted = false; + MuteIsSubscribed = true; + } + + MuteFeedback.FireUpdate(); + } + else if (this.HasLevel && customName == LevelCustomName) + { + //if (value.IndexOf("+OK") > -1) + //{ + // int pointer = value.IndexOf(" +OK"); + + // LevelIsSubscribed = true; + + //} + + var _value = Double.Parse(value); + + _VolumeLevel = (ushort)Scale(_value, MinLevel, MaxLevel, 0, 65535); + + LevelIsSubscribed = true; + + VolumeLevelFeedback.FireUpdate(); + } + + } + + /// + /// Parses a non subscription response + /// + /// The attribute code of the command + /// The message to parse + public override void ParseGetMessage(string attributeCode, string message) + { + try + { + // Parse an "+OK" message + string pattern = @"\+OK ""value"":(.*)"; + + Match match = Regex.Match(message, pattern); + + if (match.Success) + { + + string value = match.Groups[1].Value; + + Debug.Console(1, this, "Response: '{0}' Value: '{1}'", attributeCode, value); + + if (message.IndexOf("\"value\":") > -1) + { + switch (attributeCode) + { + case "minLevel": + { + MinLevel = Double.Parse(value); + + Debug.Console(1, this, "MinLevel is '{0}'", MinLevel); + + break; + } + case "maxLevel": + { + MaxLevel = Double.Parse(value); + + Debug.Console(1, this, "MaxLevel is '{0}'", MaxLevel); + + break; + } + default: + { + Debug.Console(2, "Response does not match expected attribute codes: '{0}'", message); + + break; + } + } + } + } + } + catch (Exception e) + { + Debug.Console(2, "Unable to parse message: '{0}'\n{1}", message, e); + } + + } + + /// + /// Turns the mute off + /// + public void MuteOff() + { + SendFullCommand("set", "mute", "false"); + } + + /// + /// Turns the mute on + /// + public void MuteOn() + { + SendFullCommand("set", "mute", "true"); + } + + /// + /// Sets the volume to a specified level + /// + /// + public void SetVolume(ushort level) + { + Debug.Console(1, this, "volume: {0}", level); + // Unmute volume if new level is higher than existing + if (level > _VolumeLevel && AutomaticUnmuteOnVolumeUp) + if(_IsMuted) + MuteOff(); + + double volumeLevel = Scale(level, 0, 65535, MinLevel, MaxLevel); + + SendFullCommand("set", "level", string.Format("{0:0.000000}", volumeLevel)); + } + + /// + /// Toggles mute status + /// + public void MuteToggle() + { + SendFullCommand("toggle", "mute", ""); + } + + /// + /// Decrements volume level + /// + /// + public void VolumeDown(bool pressRelease) + { + SendFullCommand("decrement", "level", ""); + } + + /// + /// Increments volume level + /// + /// + public void VolumeUp(bool pressRelease) + { + SendFullCommand("increment", "level", ""); + + if (AutomaticUnmuteOnVolumeUp) + if (!_IsMuted) + MuteOff(); + } + + ///// + ///// Scales the input from the input range to the output range + ///// + ///// + ///// + ///// + ///// + ///// + ///// + //int Scale(int input, int inMin, int inMax, int outMin, int outMax) + //{ + // Debug.Console(1, this, "Scaling (int) input '{0}' with min '{1}'/max '{2}' to output range min '{3}'/max '{4}'", input, inMin, inMax, outMin, outMax); + + // int inputRange = inMax - inMin; + + // int outputRange = outMax - outMin; + + // var output = (((input-inMin) * outputRange) / inputRange ) - outMin; + + // Debug.Console(1, this, "Scaled output '{0}'", output); + + // return output; + //} + + /// + /// Scales the input from the input range to the output range + /// + /// + /// + /// + /// + /// + /// + double Scale(double input, double inMin, double inMax, double outMin, double outMax) + { + Debug.Console(1, this, "Scaling (double) input '{0}' with min '{1}'/max '{2}' to output range min '{3}'/max '{4}'",input ,inMin ,inMax ,outMin, outMax); + + double inputRange = inMax - inMin; + + if (inputRange <= 0) + { + throw new ArithmeticException(string.Format("Invalid Input Range '{0}' for Scaling. Min '{1}' Max '{2}'.", inputRange, inMin, inMax)); + } + + double outputRange = outMax - outMin; + + var output = (((input - inMin) * outputRange) / inputRange) + outMin; + + Debug.Console(1, this, "Scaled output '{0}'", output); + + return output; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraFortePropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraFortePropertiesConfig.cs new file mode 100644 index 00000000..1b4ae8a1 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/BiampTesiraFortePropertiesConfig.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + /// + /// + /// + public class BiampTesiraFortePropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public ControlPropertiesConfig Control { get; set; } + + /// + /// These are key-value pairs, string id, string type. + /// Valid types are level and mute. + /// Need to include the index values somehow + /// + public Dictionary LevelControlBlocks { get; set; } + // public Dictionary DialerControlBlocks {get; set;} + } + + public class BiampTesiraForteLevelControlBlockConfig + { + public bool Enabled { get; set; } + public string Label { get; set; } + public string InstanceTag { get; set; } + public int Index1 { get; set; } + public int Index2 { get; set; } + public bool HasMute { get; set; } + public bool HasLevel { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/TesiraForteControlPoint.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/TesiraForteControlPoint.cs new file mode 100644 index 00000000..a509be2a --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/TesiraForteControlPoint.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + public abstract class TesiraForteControlPoint : DspControlPoint + { + public string Key { get; protected set; } + + public string InstanceTag { get; set; } + public int Index1 { get; private set; } + public int Index2 { get; private set; } + public BiampTesiraForteDsp Parent { get; private set; } + + public bool IsSubscribed { get; protected set; } + + protected TesiraForteControlPoint(string id, int index1, int index2, BiampTesiraForteDsp parent) + { + InstanceTag = id; + Index1 = index1; + Index2 = index2; + Parent = parent; + } + + virtual public void Initialize() + { + } + + /// + /// Sends a command to the DSP + /// + /// command + /// attribute code + /// value (use "" if not applicable) + public virtual void SendFullCommand(string command, string attributeCode, string value) + { + // Command Format: InstanceTag get/set/toggle/increment/decrement/subscribe/unsubscribe attributeCode [index] [value] + // Ex: "RoomLevel set level 1.00" + + string cmd; + + if (attributeCode == "level" || attributeCode == "mute" || attributeCode == "minLevel" || attributeCode == "maxLevel" || attributeCode == "label" || attributeCode == "rampInterval" || attributeCode == "rampStep") + { + //Command requires Index + + if (String.IsNullOrEmpty(value)) + { + // format command without value + cmd = string.Format("{0} {1} {2} {3}", InstanceTag, command, attributeCode, Index1); + } + else + { + // format commadn with value + cmd = string.Format("{0} {1} {2} {3} {4}", InstanceTag, command, attributeCode, Index1, value); + } + + } + else + { + //Command does not require Index + + if (String.IsNullOrEmpty(value)) + { + cmd = string.Format("{0} {1} {2} {3}", InstanceTag, command, attributeCode, value); + } + else + { + cmd = string.Format("{0} {1} {2}", InstanceTag, command, attributeCode); + } + } + + if (command == "get") + { + // This command will generate a return value response so it needs to be queued + Parent.EnqueueCommand(new BiampTesiraForteDsp.QueuedCommand{ Command = cmd, AttributeCode = attributeCode, ControlPoint = this }); + } + else + { + // This command will generate a simple "+OK" response and doesn't need to be queued + Parent.SendLine(cmd); + } + + } + + virtual public void ParseGetMessage(string attributeCode, string message) + { + } + + + + public virtual void SendSubscriptionCommand(string customName, string attributeCode, int responseRate) + { + // Subscription string format: InstanceTag subscribe attributeCode Index1 customName responseRate + // Ex: "RoomLevel subscribe level 1 MyRoomLevel 500" + + string cmd; + + if (responseRate > 0) + { + cmd = string.Format("{0} subscribe {1} {2} {3} {4}", InstanceTag, attributeCode, Index1, customName, responseRate); + } + else + { + cmd = string.Format("{0} subscribe {1} {2} {3}", InstanceTag, attributeCode, Index1, customName); + } + + Parent.SendLine(cmd); + } + + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/TesiraForteMuteControl.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/TesiraForteMuteControl.cs new file mode 100644 index 00000000..82689408 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/BiampTesira/TesiraForteMuteControl.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + + //QUESTIONS: + + //When subscribing, just use the Instance ID for Custom Name? + + //Verbose on subscriptions? + + //! "publishToken":"name" "value":-77.0 + //! "myLevelName" -77 + + //public class TesiraForteMuteControl : IDspLevelControl + //{ + // BiampTesiraForteDsp Parent; + // bool _IsMuted; + // ushort _VolumeLevel; + + // public TesiraForteMuteControl(string id, BiampTesiraForteDsp parent) + // : base(id) + // { + // Parent = parent; + // } + + // public void Initialize() + // { + + // } + + // protected override Func MuteFeedbackFunc + // { + // get { return () => _IsMuted; } + // } + + // protected override Func VolumeLevelFeedbackFunc + // { + // get { return () => _VolumeLevel; } + // } + + // public override void MuteOff() + // { + // throw new NotImplementedException(); + // } + + // public override void MuteOn() + // { + // throw new NotImplementedException(); + // } + + // public override void SetVolume(ushort level) + // { + // throw new NotImplementedException(); + // } + + // public override void MuteToggle() + // { + // } + + // public override void VolumeDown(bool pressRelease) + // { + // throw new NotImplementedException(); + // } + + // public override void VolumeUp(bool pressRelease) + // { + // throw new NotImplementedException(); + // } + //} +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/DspBase.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/DspBase.cs new file mode 100644 index 00000000..36c655d2 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/DspBase.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + public abstract class DspBase : Device + { + public Dictionary LevelControlPoints { get; private set; } + + public Dictionary DialerControlPoints { get; private set; } + + public Dictionary SwitcherControlPoints { get; private set; } + + public abstract void RunPreset(string name); + + public DspBase(string key, string name) : + base(key, name) { } + + + // in audio call feedback + + // VOIP + // Phone dialer + } + + // Fusion + // Privacy state + // Online state + // level/mutes ? + + // AC Log call stats + + // Typical presets: + // call default preset to restore levels and mutes + + public abstract class DspControlPoint + { + string Key { get; set; } + } + + + public class DspDialerBase + { + + } + + + // Main program + // VTC + // ATC + // Mics, unusual + + public interface IBiampTesiraDspLevelControl : IBasicVolumeWithFeedback + { + /// + /// In BiAmp: Instance Tag, QSC: Named Control, Polycom: + /// + string ControlPointTag { get; } +#warning I dont think index1 and index2 should be a part of the interface. JTA 2018-07-17 + int Index1 { get; } + int Index2 { get; } + bool HasMute { get; } + bool HasLevel { get; } + bool AutomaticUnmuteOnVolumeUp { get; } + } + + + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/PolycomSoundStructure/SoundStructureBasics.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/PolycomSoundStructure/SoundStructureBasics.cs new file mode 100644 index 00000000..92af6a97 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/PolycomSoundStructure/SoundStructureBasics.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + public class SoundStructureBasics + { + // public Dictionary Levels { get; private set; } + + + } + + + +} + diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDsp.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDsp.cs new file mode 100644 index 00000000..24fd93d4 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDsp.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; +using System.Text.RegularExpressions; +using Crestron.SimplSharp.Reflection; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + + // QUESTIONS: + // + // When subscribing, just use the Instance ID for Custom Name? + + // Verbose on subscriptions? + + // Example subscription feedback responses + // ! "publishToken":"name" "value":-77.0 + // ! "myLevelName" -77 + + public class QscDsp : DspBase + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public GenericCommunicationMonitor CommunicationMonitor { get; private set; } + + new public Dictionary LevelControlPoints { get; private set; } + new public Dictionary Dialers { get; set; } + public List PresetList = new List(); + + public bool isSubscribed; + + + + CrestronQueue CommandQueue; + + bool CommandQueueInProgress = false; + + + public bool ShowHexResponse { get; set; } + + public QscDsp(string key, string name, IBasicCommunication comm, QscDspPropertiesConfig props) : + base(key, name) + { + CommandQueue = new CrestronQueue(100); + + Communication = comm; + var socket = comm as ISocketStatus; + if (socket != null) + { + // This instance uses IP control + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + else + { + // This instance uses RS-232 control + } + PortGather = new CommunicationGather(Communication, "\x0a"); + PortGather.LineReceived += this.Port_LineReceived; + + LevelControlPoints = new Dictionary(); + Dialers = new Dictionary(); + + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 20000, 120000, 300000, "cgp 1\x0D\x0A"); + } + + + foreach (KeyValuePair block in props.LevelControlBlocks) + { + this.LevelControlPoints.Add(block.Key, new QscDspLevelControl(block.Key, block.Value, this)); + Debug.Console(2, this, "Added LevelControlPoint {0}", block.Key); + + + } + foreach (KeyValuePair preset in props.presets) + { + this.addPreset(preset.Value); + Debug.Console(2, this, "Added Preset {0} {1}", preset.Value.label, preset.Value.preset); + } + foreach (KeyValuePair dialerConfig in props.dialerControlBlocks) + { + Debug.Console(2, this, "Added Dialer {0}\n {1}", dialerConfig.Key, dialerConfig.Value); + this.Dialers.Add(dialerConfig.Key, new QscDspDialer(dialerConfig.Value, this)); + + } + + } + + public override bool CustomActivate() + { + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + + CrestronConsole.AddNewConsoleCommand(SendLine, "send" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + return true; + } + + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.Console(2, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); + + if (e.Client.IsConnected) + { + SubscribeToAttributes(); + } + else + { + // Cleanup items from this session + CommandQueue.Clear(); + CommandQueueInProgress = false; + } + } + + /// + /// Initiates the subscription process to the DSP + /// + void SubscribeToAttributes() + { + SendLine("cgd 1"); + + SendLine("cgc 1"); + + foreach (KeyValuePair level in LevelControlPoints) + { + level.Value.Subscribe(); + } + + foreach (var dialer in Dialers) + { + dialer.Value.Subscribe(); + } + + if (CommunicationMonitor != null) + { + + CommunicationMonitor.Start(); + //CommunicationMonitor = null; + } + else + { + + } + //CommunicationMonitor.Message = "cgp 1\x0D\x0A"; + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + //CommunicationMonitor.Start(); + if (!CommandQueueInProgress) + SendNextQueuedCommand(); + + // ResetSubscriptionTimer(); + } + + + + /// + /// Handles a response message from the DSP + /// + /// + /// + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + Debug.Console(2, this, "RX: '{0}'", args.Text); + try + { + if (args.Text.IndexOf("sr ") > -1) + { + } + else if (args.Text.IndexOf("cv") > -1) + { + + var changeMessage = args.Text.Split(null); + + string changedInstance = changeMessage[1].Replace("\"", ""); + Debug.Console(1, this, "cv parse Instance: {0}", changedInstance); + bool foundItFlag = false; + foreach (KeyValuePair controlPoint in LevelControlPoints) + { + if (changedInstance == controlPoint.Value.LevelInstanceTag) + { + controlPoint.Value.ParseSubscriptionMessage(changedInstance, changeMessage[4]); + foundItFlag = true; + return; + } + else if (changedInstance == controlPoint.Value.MuteInstanceTag) + { + controlPoint.Value.ParseSubscriptionMessage(changedInstance, changeMessage[2].Replace("\"", "")); + foundItFlag = true; + return; + } + + } + if (!foundItFlag) + { + foreach (var dialer in Dialers) + { + PropertyInfo[] properties = dialer.Value.Tags.GetType().GetCType().GetProperties(); + //GetPropertyValues(Tags); + foreach (var prop in properties) + { + var propValue = prop.GetValue(dialer.Value.Tags, null) as string; + if (changedInstance == propValue) + { + dialer.Value.ParseSubscriptionMessage(changedInstance, changeMessage[2].Replace("\"", "")); + foundItFlag = true; + return; + } + } + if (foundItFlag) + { + return; + } + } + } + + } + + + } + catch (Exception e) + { + if (Debug.Level == 2) + Debug.Console(2, this, "Error parsing response: '{0}'\n{1}", args.Text, e); + } + + } + + /// + /// Sends a command to the DSP (with delimiter appended) + /// + /// Command to send + public void SendLine(string s) + { + Debug.Console(1, this, "TX: '{0}'", s); + Communication.SendText(s + "\x0a"); + } + + /// + /// Adds a command from a child module to the queue + /// + /// Command object from child module + public void EnqueueCommand(QueuedCommand commandToEnqueue) + { + CommandQueue.Enqueue(commandToEnqueue); + //Debug.Console(1, this, "Command (QueuedCommand) Enqueued '{0}'. CommandQueue has '{1}' Elements.", commandToEnqueue.Command, CommandQueue.Count); + + if(!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Adds a raw string command to the queue + /// + /// + public void EnqueueCommand(string command) + { + CommandQueue.Enqueue(command); + //Debug.Console(1, this, "Command (string) Enqueued '{0}'. CommandQueue has '{1}' Elements.", command, CommandQueue.Count); + + if (!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Sends the next queued command to the DSP + /// + void SendNextQueuedCommand() + { + if (Communication.IsConnected && !CommandQueue.IsEmpty) + { + CommandQueueInProgress = true; + + if (CommandQueue.Peek() is QueuedCommand) + { + QueuedCommand nextCommand = new QueuedCommand(); + + nextCommand = (QueuedCommand)CommandQueue.Peek(); + + SendLine(nextCommand.Command); + } + else + { + string nextCommand = (string)CommandQueue.Peek(); + + SendLine(nextCommand); + } + } + + } + + public void RunPresetNumber(ushort n) + { + RunPreset(PresetList[n].preset); + } + + public void addPreset(QscDspPresets s) + { + PresetList.Add(s); + } + /// + /// Sends a command to execute a preset + /// + /// Preset Name + public override void RunPreset(string name) + { + SendLine(string.Format("ssl {0}", name)); + SendLine("cgp 1"); + } + + public class QueuedCommand + { + public string Command { get; set; } + public string AttributeCode { get; set; } + public QscDspControlPoint ControlPoint { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspControlPoint.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspControlPoint.cs new file mode 100644 index 00000000..542336eb --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspControlPoint.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + public abstract class QscDspControlPoint : DspControlPoint + { + public string Key { get; protected set; } + + public string LevelInstanceTag { get; set; } + public string MuteInstanceTag { get; set; } + public QscDsp Parent { get; private set; } + + public bool IsSubscribed { get; protected set; } + + protected QscDspControlPoint(string levelInstanceTag, string muteInstanceTag, QscDsp parent) + { + LevelInstanceTag = levelInstanceTag; + MuteInstanceTag = muteInstanceTag; + Parent = parent; + } + + virtual public void Initialize() + { + } + + /// + /// Sends a command to the DSP + /// + /// command + /// attribute code + /// value (use "" if not applicable) + public virtual void SendFullCommand(string cmd, string instance, string value) + { + + var cmdToSemd = string.Format("{0} {1} {2}", cmd, instance, value); + + Parent.SendLine(cmdToSemd); + + } + + virtual public void ParseGetMessage(string attributeCode, string message) + { + } + + + + public virtual void SendSubscriptionCommand(string instanceTag, string changeGroup) + { + // Subscription string format: InstanceTag subscribe attributeCode Index1 customName responseRate + // Ex: "RoomLevel subscribe level 1 MyRoomLevel 500" + + string cmd; + + cmd = string.Format("cga {0} {1}", changeGroup, instanceTag); + + Parent.SendLine(cmd); + } + + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspDialer.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspDialer.cs new file mode 100644 index 00000000..387dffe7 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspDialer.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using Crestron.SimplSharp.Reflection; +using Crestron.SimplSharp; +using PepperDash.Essentials.Devices.Common.Codec; +using Crestron.SimplSharpPro.CrestronThread; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + public class QscDspDialer : DspDialerBase, IHasDialer + { + public QscDialerConfig Tags; + public bool IsInCall { get; private set; } + public QscDsp Parent { get; private set; } + public string DialString { get; private set; } + public bool OffHook { get; private set; } + public bool AutoAnswerState { get; private set; } + public bool DoNotDisturbState { get; private set; } + + public BoolFeedback OffHookFeedback; + public BoolFeedback AutoAnswerFeedback; + public BoolFeedback DoNotDisturbFeedback; + public StringFeedback DialStringFeedback; + + // Add requirements for Dialer functionality + + public QscDspDialer(QscDialerConfig Config, QscDsp parent) + { + Tags = Config; + Parent = parent; + DialStringFeedback = new StringFeedback(() => { return DialString; }); + OffHookFeedback = new BoolFeedback(() => { return OffHook; }); + AutoAnswerFeedback = new BoolFeedback(() => { return AutoAnswerState; }); + DoNotDisturbFeedback = new BoolFeedback(() => { return DoNotDisturbState; }); + } + + public event EventHandler CallStatusChange; + + public void Subscribe() + { + try + { + // Do subscriptions and blah blah + // This would be better using reflection JTA 2018-08-28 + PropertyInfo[] properties = Tags.GetType().GetCType().GetProperties(); + //GetPropertyValues(Tags); + + Debug.Console(2, "QscDspDialer Subscribe"); + foreach (var prop in properties) + { + //var val = prop.GetValue(obj, null); + + + Debug.Console(2, "Property {0}, {1}, {2}\n", prop.GetType().Name, prop.Name, prop.PropertyType.FullName); + if (prop.Name.Contains("Tag") && !prop.Name.Contains("keypad")) + { + var propValue = prop.GetValue(Tags, null) as string; + Debug.Console(2, "Property {0}, {1}, {2}\n", prop.GetType().Name, prop.Name, propValue); + SendSubscriptionCommand(propValue, "1"); + } + + + } + } + catch (Exception e) + { + + Debug.Console(2, "QscDspDialer Subscription Error: '{0}'\n", e); + } + + + // SendSubscriptionCommand(, "1"); + // SendSubscriptionCommand(config. , "mute", 500); + } + public void ParseSubscriptionMessage(string customName, string value) + { + + // Check for valid subscription response + Debug.Console(1, "QscDialerTag {0} Response: '{1}'", customName, value); + if (customName == Tags.dialStringTag) + { + Debug.Console(2, "QscDialerTag DialStringChanged ", value); + this.DialString = value; + this.DialStringFeedback.FireUpdate(); + } + else if (customName == Tags.doNotDisturbTag) + { + if (value == "on") + { + this.DoNotDisturbState = true; + } + else if (value == "off") + { + this.DoNotDisturbState = false; + } + DoNotDisturbFeedback.FireUpdate(); + } + else if (customName == Tags.callStatusTag) + { + if (value == "Dialing") + { + this.OffHook = true; + } + else if (value == "Disconnected") + { + this.OffHook = false; + if (Tags.ClearOnHangup) + { + this.SendKeypad(eKeypadKeys.Clear); + } + } + this.OffHookFeedback.FireUpdate(); + } + else if (customName == Tags.autoAnswerTag) + { + if (value == "on") + { + this.AutoAnswerState = true; + } + else if (value == "off") + { + this.AutoAnswerState = false; + } + AutoAnswerFeedback.FireUpdate(); + } + else if (customName == Tags.hookStatusTag) + { + if (value == "true") + { + this.OffHook = true; + } + else if (value == "false") + { + this.OffHook = false; + } + this.OffHookFeedback.FireUpdate(); + } + + + } + + public void DoNotDisturbToggle() + { + int dndStateInt = !DoNotDisturbState ? 1 : 0; + Parent.SendLine(string.Format("csv {0} {1}", Tags.doNotDisturbTag, dndStateInt)); + } + public void DoNotDisturbOn() + { + Parent.SendLine(string.Format("csv {0} 1", Tags.doNotDisturbTag)); + } + public void DoNotDisturbOff() + { + Parent.SendLine(string.Format("csv {0} 0", Tags.doNotDisturbTag)); + } + public void AutoAnswerToggle() + { + int autoAnswerStateInt = !AutoAnswerState ? 1 : 0; + Parent.SendLine(string.Format("csv {0} {1}", Tags.autoAnswerTag, autoAnswerStateInt)); + } + public void AutoAnswerOn() + { + Parent.SendLine(string.Format("csv {0} 1", Tags.autoAnswerTag)); + } + public void AutoAnswerOff() + { + Parent.SendLine(string.Format("csv {0} 0", Tags.autoAnswerTag)); + } + + private void PollKeypad() + { + Thread.Sleep(50); + Parent.SendLine(string.Format("cg {0}", Tags.dialStringTag)); + } + + public void SendKeypad(eKeypadKeys button) + { + string keypadTag = null; + switch (button) + { + case eKeypadKeys.Num0: keypadTag = Tags.keypad0Tag; break; + case eKeypadKeys.Num1: keypadTag = Tags.keypad1Tag; break; + case eKeypadKeys.Num2: keypadTag = Tags.keypad2Tag; break; + case eKeypadKeys.Num3: keypadTag = Tags.keypad3Tag; break; + case eKeypadKeys.Num4: keypadTag = Tags.keypad4Tag; break; + case eKeypadKeys.Num5: keypadTag = Tags.keypad5Tag; break; + case eKeypadKeys.Num6: keypadTag = Tags.keypad6Tag; break; + case eKeypadKeys.Num7: keypadTag = Tags.keypad7Tag; break; + case eKeypadKeys.Num8: keypadTag = Tags.keypad8Tag; break; + case eKeypadKeys.Num9: keypadTag = Tags.keypad9Tag; break; + case eKeypadKeys.Pound: keypadTag = Tags.keypadPoundTag; break; + case eKeypadKeys.Star: keypadTag = Tags.keypadStarTag; break; + case eKeypadKeys.Backspace: keypadTag = Tags.keypadBackspaceTag; break; + case eKeypadKeys.Clear: keypadTag = Tags.keypadClearTag; break; + } + if (keypadTag != null) + { + var cmdToSend = string.Format("ct {0}", keypadTag); + Parent.SendLine(cmdToSend); + PollKeypad(); + } + } + public void SendSubscriptionCommand(string instanceTag, string changeGroup) + { + // Subscription string format: InstanceTag subscribe attributeCode Index1 customName responseRate + // Ex: "RoomLevel subscribe level 1 MyRoomLevel 500" + + var cmd = string.Format("cga {0} {1}", changeGroup, instanceTag); + + Parent.SendLine(cmd); + } + public void Dial() + { + if (!this.OffHook) + { + Parent.SendLine(string.Format("ct {0}", Tags.connectTag)); + } + else + { + Parent.SendLine(string.Format("ct {0}", Tags.disconnectTag)); + } + Thread.Sleep(50); + Parent.SendLine(string.Format("cg {0}", Tags.callStatusTag)); + } + public void Dial(string number) + { + } + public void EndCall(CodecActiveCallItem activeCall) + { + } + public void EndAllCalls() + { + } + public void AcceptCall(CodecActiveCallItem item) + { + } + + public void RejectCall(CodecActiveCallItem item) + { + + } + + public void SendDtmf(string digit) + { + + } + + public enum eKeypadKeys + { + Num1, + Num2, + Num3, + Num4, + Num5, + Num6, + Num7, + Num8, + Num9, + Num0, + Star, + Pound, + Clear, + Backspace + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspLevelControl.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspLevelControl.cs new file mode 100644 index 00000000..498127f5 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspLevelControl.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System.Text.RegularExpressions; + + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + public class QscDspLevelControl : QscDspControlPoint, IBasicVolumeWithFeedback, IKeyed + { + bool _IsMuted; + ushort _VolumeLevel; + + public BoolFeedback MuteFeedback { get; private set; } + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public bool Enabled { get; set; } + public ePdtLevelTypes Type; + CTimer VolumeUpRepeatTimer; + CTimer VolumeDownRepeatTimer; + + /// + /// Used for to identify level subscription values + /// + public string LevelCustomName { get; private set; } + + /// + /// Used for to identify mute subscription values + /// + public string MuteCustomName { get; private set; } + + /// + /// Minimum fader level + /// + double MinLevel; + + /// + /// Maximum fader level + /// + double MaxLevel; + + /// + /// Checks if a valid subscription string has been recieved for all subscriptions + /// + public bool IsSubsribed + { + get + { + bool isSubscribed = false; + + if (HasMute && MuteIsSubscribed) + isSubscribed = true; + + if (HasLevel && LevelIsSubscribed) + isSubscribed = true; + + return isSubscribed; + } + } + + public bool AutomaticUnmuteOnVolumeUp { get; private set; } + + public bool HasMute { get; private set; } + + public bool HasLevel { get; private set; } + + bool MuteIsSubscribed; + + bool LevelIsSubscribed; + + //public TesiraForteLevelControl(string label, string id, int index1, int index2, bool hasMute, bool hasLevel, BiampTesiraForteDsp parent) + // : base(id, index1, index2, parent) + //{ + // Initialize(label, hasMute, hasLevel); + //} + + public QscDspLevelControl(string key, QscDspLevelControlBlockConfig config, QscDsp parent) + : base(config.LevelInstanceTag, config.MuteInstanceTag, parent) + { + if (!config.Disabled) + { + Initialize(key, config); + } + } + + + /// + /// Initializes this attribute based on config values and generates subscriptions commands and adds commands to the parent's queue. + /// + public void Initialize(string key, QscDspLevelControlBlockConfig config) + { + Key = string.Format("{0}--{1}", Parent.Key, key); + Enabled = true; + DeviceManager.AddDevice(this); + if (config.IsMic) + { + Type = ePdtLevelTypes.microphone; + } + else + { + Type = ePdtLevelTypes.speaker; + } + + Debug.Console(2, this, "Adding LevelControl '{0}'", Key); + + this.IsSubscribed = false; + + MuteFeedback = new BoolFeedback(() => _IsMuted); + + VolumeLevelFeedback = new IntFeedback(() => _VolumeLevel); + + VolumeUpRepeatTimer = new CTimer(VolumeUpRepeat, Timeout.Infinite); + VolumeDownRepeatTimer = new CTimer(VolumeDownRepeat, Timeout.Infinite); + LevelCustomName = config.Label; + HasMute = config.HasMute; + HasLevel = config.HasLevel; + } + + public void Subscribe() + { + // Do subscriptions and blah blah + + // Subscribe to mute + if (this.HasMute) + { + + SendSubscriptionCommand(this.MuteInstanceTag, "1"); + // SendSubscriptionCommand(config. , "mute", 500); + } + + // Subscribe to level + if (this.HasLevel) + { + + SendSubscriptionCommand(this.LevelInstanceTag, "1"); + // SendSubscriptionCommand(this.con, "level", 250); + + //SendFullCommand("get", "minLevel", null); + + //SendFullCommand("get", "maxLevel", null); + } + } + + + /// + /// Parses the response from the DspBase + /// + /// + /// + public void ParseSubscriptionMessage(string customName, string value) + { + + // Check for valid subscription response + Debug.Console(1, this, "Level {0} Response: '{1}'", customName, value); + if (customName == MuteInstanceTag) + { + if (value == "muted") + { + _IsMuted = true; + MuteIsSubscribed = true; + + } + else if (value == "unmuted") + { + _IsMuted = false; + MuteIsSubscribed = true; + } + + MuteFeedback.FireUpdate(); + } + else if (customName == LevelInstanceTag) + { + + + var _value = Double.Parse(value); + + _VolumeLevel = (ushort)(_value * 65535); + Debug.Console(1, this, "Level {0} VolumeLevel: '{1}'", customName, _VolumeLevel); + LevelIsSubscribed = true; + + VolumeLevelFeedback.FireUpdate(); + } + + } + + + /// + /// Turns the mute off + /// + public void MuteOff() + { + SendFullCommand("csv", this.MuteInstanceTag, "0"); + } + + /// + /// Turns the mute on + /// + public void MuteOn() + { + SendFullCommand("csv", this.MuteInstanceTag, "1"); + } + + /// + /// Sets the volume to a specified level + /// + /// + public void SetVolume(ushort level) + { + Debug.Console(1, this, "volume: {0}", level); + // Unmute volume if new level is higher than existing + if (AutomaticUnmuteOnVolumeUp && _IsMuted) + { + MuteOff(); + } + double newLevel = Scale(level); + Debug.Console(1, this, "newVolume: {0}", newLevel); + SendFullCommand("csp", this.LevelInstanceTag, string.Format("{0}", newLevel)); + } + + /// + /// Toggles mute status + /// + public void MuteToggle() + { + + if (_IsMuted) + { + SendFullCommand("csv", this.MuteInstanceTag, "0"); + } + else + { + SendFullCommand("csv", this.MuteInstanceTag, "1"); + } + + } + + public void VolumeUpRepeat(object callbackObject) + { + this.VolumeUp(true); + } + public void VolumeDownRepeat(object callbackObject) + { + this.VolumeDown(true); + } + + public void VolumeDown(bool press) + { + + + if (press) + { + VolumeDownRepeatTimer.Reset(100); + SendFullCommand("css ", this.LevelInstanceTag, "--"); + + + } + else + { + VolumeDownRepeatTimer.Stop(); + // VolumeDownRepeatTimer.Dispose(); + } + } + + /// + /// Increments volume level + /// + /// + public void VolumeUp(bool press) + { + if (press) + { + VolumeUpRepeatTimer.Reset(100); + SendFullCommand("css ", this.LevelInstanceTag, "++"); + + if (AutomaticUnmuteOnVolumeUp) + if (!_IsMuted) + MuteOff(); + } + else + { + VolumeUpRepeatTimer.Stop(); + } + } + /// + double Scale(double input) + { + Debug.Console(1, this, "Scaling (double) input '{0}'",input ); + + var output = (input / 65535); + + Debug.Console(1, this, "Scaled output '{0}'", output); + + return output; + } + } + public enum ePdtLevelTypes + { + speaker = 0, + microphone = 1 + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspPropertiesConfig.cs new file mode 100644 index 00000000..9eab3e84 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QSC/QscDspPropertiesConfig.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + /// + /// + /// + public class QscDspPropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public ControlPropertiesConfig Control { get; set; } + + /// + /// These are key-value pairs, string id, string type. + /// Valid types are level and mute. + /// Need to include the index values somehow + /// + public Dictionary LevelControlBlocks { get; set; } + public Dictionary dialerControlBlocks { get; set; } + public Dictionary presets { get; set; } + // public Dictionary DialerControlBlocks {get; set;} + } + public interface IQscDspBasicLevel : IBasicVolumeWithFeedback + { + /// + /// In BiAmp: Instance Tag, QSC: Named Control, Polycom: + /// + string LevelInstanceTag { get; set; } + string MuteInstanceTag { get; set; } + bool HasMute { get; } + bool HasLevel { get; } + bool AutomaticUnmuteOnVolumeUp { get; } + } + public class QscDspLevelControlBlockConfig + { + public bool Disabled { get; set; } + public string Label { get; set; } + public string LevelInstanceTag { get; set; } + public string MuteInstanceTag { get; set; } + public bool HasMute { get; set; } + public bool HasLevel { get; set; } + public bool IsMic { get; set; } + } + + public class QscDialerConfig + { + public string incomingCallRingerTag {get; set;} + public string dialStringTag {get; set;} + public string disconnectTag {get; set;} + public string connectTag {get; set;} + public string callStatusTag {get; set;} + public string hookStatusTag {get; set;} + public string doNotDisturbTag { get; set; } + public string autoAnswerTag { get; set; } + + public string keypadBackspaceTag {get; set;} + public string keypadClearTag {get; set;} + public string keypad1Tag {get; set;} + public string keypad2Tag {get; set;} + public string keypad3Tag {get; set;} + public string keypad4Tag {get; set;} + public string keypad5Tag {get; set;} + public string keypad6Tag {get; set;} + public string keypad7Tag {get; set;} + public string keypad8Tag {get; set;} + public string keypad9Tag {get; set;} + public string keypad0Tag {get; set;} + public string keypadPoundTag {get; set;} + public string keypadStarTag {get; set;} + + public bool ClearOnHangup { get; set; } + + } + + public class QscDspPresets + { + public string label { get; set; } + public string preset { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/._QscDspPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/._QscDspPropertiesConfig.cs new file mode 100644 index 0000000000000000000000000000000000000000..8801a8a6408ff8cd8381b38d9ad99de01c2fa8c8 GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vn!Wq+ zUWg8c1wiEjXj&M7e5kloa(=E}VnIPps$NcFaY<=$YD!9CNvd){Fi4N;S@9SU&9Eg@ z5=@U0qaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsfFlGzolOu11G$il%wmP2 z)Z+ZoqU2PCwEUuMh0MH?)Vz|+{Jg}RoJxh9)U* LevelControlPoints { get; private set; } + new public Dictionary Dialers { get; set; } + public List PresetList = new List(); + + public bool isSubscribed; + + + + CrestronQueue CommandQueue; + + bool CommandQueueInProgress = false; + + + public bool ShowHexResponse { get; set; } + + public QscDsp(string key, string name, IBasicCommunication comm, QscDspPropertiesConfig props) : + base(key, name) + { + CommandQueue = new CrestronQueue(100); + + Communication = comm; + var socket = comm as ISocketStatus; + if (socket != null) + { + // This instance uses IP control + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + else + { + // This instance uses RS-232 control + } + PortGather = new CommunicationGather(Communication, "\x0a"); + PortGather.LineReceived += this.Port_LineReceived; + + LevelControlPoints = new Dictionary(); + Dialers = new Dictionary(); + + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 20000, 120000, 300000, "cgp 1\x0D\x0A"); + } + + + foreach (KeyValuePair block in props.LevelControlBlocks) + { + this.LevelControlPoints.Add(block.Key, new QscDspLevelControl(block.Key, block.Value, this)); + Debug.Console(2, this, "Added LevelControlPoint {0}", block.Key); + + + } + foreach (KeyValuePair preset in props.presets) + { + this.addPreset(preset.Value); + Debug.Console(2, this, "Added Preset {0} {1}", preset.Value.label, preset.Value.preset); + } + foreach (KeyValuePair dialerConfig in props.dialerControlBlocks) + { + Debug.Console(2, this, "Added Dialer {0}\n {1}", dialerConfig.Key, dialerConfig.Value); + this.Dialers.Add(dialerConfig.Key, new QscDspDialer(dialerConfig.Value, this)); + + } + + } + + public override bool CustomActivate() + { + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + + CrestronConsole.AddNewConsoleCommand(SendLine, "send" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + return true; + } + + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.Console(2, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); + + if (e.Client.IsConnected) + { + SubscribeToAttributes(); + } + else + { + // Cleanup items from this session + CommandQueue.Clear(); + CommandQueueInProgress = false; + } + } + + /// + /// Initiates the subscription process to the DSP + /// + void SubscribeToAttributes() + { + SendLine("cgd 1"); + + SendLine("cgc 1"); + + foreach (KeyValuePair level in LevelControlPoints) + { + level.Value.Subscribe(); + } + + foreach (var dialer in Dialers) + { + dialer.Value.Subscribe(); + } + + if (CommunicationMonitor != null) + { + + CommunicationMonitor.Start(); + //CommunicationMonitor = null; + } + else + { + + } + //CommunicationMonitor.Message = "cgp 1\x0D\x0A"; + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + //CommunicationMonitor.Start(); + if (!CommandQueueInProgress) + SendNextQueuedCommand(); + + // ResetSubscriptionTimer(); + } + + + + /// + /// Handles a response message from the DSP + /// + /// + /// + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + Debug.Console(2, this, "RX: '{0}'", args.Text); + try + { + if (args.Text.IndexOf("sr ") > -1) + { + } + else if (args.Text.IndexOf("cv") > -1) + { + + var changeMessage = args.Text.Split(null); + + string changedInstance = changeMessage[1].Replace("\"", ""); + Debug.Console(1, this, "cv parse Instance: {0}", changedInstance); + bool foundItFlag = false; + foreach (KeyValuePair controlPoint in LevelControlPoints) + { + if (changedInstance == controlPoint.Value.LevelInstanceTag) + { + controlPoint.Value.ParseSubscriptionMessage(changedInstance, changeMessage[4]); + foundItFlag = true; + return; + } + else if (changedInstance == controlPoint.Value.MuteInstanceTag) + { + controlPoint.Value.ParseSubscriptionMessage(changedInstance, changeMessage[2].Replace("\"", "")); + foundItFlag = true; + return; + } + + } + if (!foundItFlag) + { + foreach (var dialer in Dialers) + { + PropertyInfo[] properties = dialer.Value.Tags.GetType().GetCType().GetProperties(); + //GetPropertyValues(Tags); + foreach (var prop in properties) + { + var propValue = prop.GetValue(dialer.Value.Tags, null) as string; + if (changedInstance == propValue) + { + dialer.Value.ParseSubscriptionMessage(changedInstance, changeMessage[2].Replace("\"", "")); + foundItFlag = true; + return; + } + } + if (foundItFlag) + { + return; + } + } + } + + } + + + } + catch (Exception e) + { + if (Debug.Level == 2) + Debug.Console(2, this, "Error parsing response: '{0}'\n{1}", args.Text, e); + } + + } + + /// + /// Sends a command to the DSP (with delimiter appended) + /// + /// Command to send + public void SendLine(string s) + { + Debug.Console(1, this, "TX: '{0}'", s); + Communication.SendText(s + "\x0a"); + } + + /// + /// Adds a command from a child module to the queue + /// + /// Command object from child module + public void EnqueueCommand(QueuedCommand commandToEnqueue) + { + CommandQueue.Enqueue(commandToEnqueue); + //Debug.Console(1, this, "Command (QueuedCommand) Enqueued '{0}'. CommandQueue has '{1}' Elements.", commandToEnqueue.Command, CommandQueue.Count); + + if(!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Adds a raw string command to the queue + /// + /// + public void EnqueueCommand(string command) + { + CommandQueue.Enqueue(command); + //Debug.Console(1, this, "Command (string) Enqueued '{0}'. CommandQueue has '{1}' Elements.", command, CommandQueue.Count); + + if (!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Sends the next queued command to the DSP + /// + void SendNextQueuedCommand() + { + if (Communication.IsConnected && !CommandQueue.IsEmpty) + { + CommandQueueInProgress = true; + + if (CommandQueue.Peek() is QueuedCommand) + { + QueuedCommand nextCommand = new QueuedCommand(); + + nextCommand = (QueuedCommand)CommandQueue.Peek(); + + SendLine(nextCommand.Command); + } + else + { + string nextCommand = (string)CommandQueue.Peek(); + + SendLine(nextCommand); + } + } + + } + + public void RunPresetNumber(ushort n) + { + RunPreset(PresetList[n].preset); + } + + public void addPreset(QscDspPresets s) + { + PresetList.Add(s); + } + /// + /// Sends a command to execute a preset + /// + /// Preset Name + public override void RunPreset(string name) + { + SendLine(string.Format("ssl {0}", name)); + SendLine("cgp 1"); + } + + public class QueuedCommand + { + public string Command { get; set; } + public string AttributeCode { get; set; } + public QscDspControlPoint ControlPoint { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspControlPoint.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspControlPoint.cs new file mode 100644 index 00000000..542336eb --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspControlPoint.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + public abstract class QscDspControlPoint : DspControlPoint + { + public string Key { get; protected set; } + + public string LevelInstanceTag { get; set; } + public string MuteInstanceTag { get; set; } + public QscDsp Parent { get; private set; } + + public bool IsSubscribed { get; protected set; } + + protected QscDspControlPoint(string levelInstanceTag, string muteInstanceTag, QscDsp parent) + { + LevelInstanceTag = levelInstanceTag; + MuteInstanceTag = muteInstanceTag; + Parent = parent; + } + + virtual public void Initialize() + { + } + + /// + /// Sends a command to the DSP + /// + /// command + /// attribute code + /// value (use "" if not applicable) + public virtual void SendFullCommand(string cmd, string instance, string value) + { + + var cmdToSemd = string.Format("{0} {1} {2}", cmd, instance, value); + + Parent.SendLine(cmdToSemd); + + } + + virtual public void ParseGetMessage(string attributeCode, string message) + { + } + + + + public virtual void SendSubscriptionCommand(string instanceTag, string changeGroup) + { + // Subscription string format: InstanceTag subscribe attributeCode Index1 customName responseRate + // Ex: "RoomLevel subscribe level 1 MyRoomLevel 500" + + string cmd; + + cmd = string.Format("cga {0} {1}", changeGroup, instanceTag); + + Parent.SendLine(cmd); + } + + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspDialer.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspDialer.cs new file mode 100644 index 00000000..387dffe7 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspDialer.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using Crestron.SimplSharp.Reflection; +using Crestron.SimplSharp; +using PepperDash.Essentials.Devices.Common.Codec; +using Crestron.SimplSharpPro.CrestronThread; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + public class QscDspDialer : DspDialerBase, IHasDialer + { + public QscDialerConfig Tags; + public bool IsInCall { get; private set; } + public QscDsp Parent { get; private set; } + public string DialString { get; private set; } + public bool OffHook { get; private set; } + public bool AutoAnswerState { get; private set; } + public bool DoNotDisturbState { get; private set; } + + public BoolFeedback OffHookFeedback; + public BoolFeedback AutoAnswerFeedback; + public BoolFeedback DoNotDisturbFeedback; + public StringFeedback DialStringFeedback; + + // Add requirements for Dialer functionality + + public QscDspDialer(QscDialerConfig Config, QscDsp parent) + { + Tags = Config; + Parent = parent; + DialStringFeedback = new StringFeedback(() => { return DialString; }); + OffHookFeedback = new BoolFeedback(() => { return OffHook; }); + AutoAnswerFeedback = new BoolFeedback(() => { return AutoAnswerState; }); + DoNotDisturbFeedback = new BoolFeedback(() => { return DoNotDisturbState; }); + } + + public event EventHandler CallStatusChange; + + public void Subscribe() + { + try + { + // Do subscriptions and blah blah + // This would be better using reflection JTA 2018-08-28 + PropertyInfo[] properties = Tags.GetType().GetCType().GetProperties(); + //GetPropertyValues(Tags); + + Debug.Console(2, "QscDspDialer Subscribe"); + foreach (var prop in properties) + { + //var val = prop.GetValue(obj, null); + + + Debug.Console(2, "Property {0}, {1}, {2}\n", prop.GetType().Name, prop.Name, prop.PropertyType.FullName); + if (prop.Name.Contains("Tag") && !prop.Name.Contains("keypad")) + { + var propValue = prop.GetValue(Tags, null) as string; + Debug.Console(2, "Property {0}, {1}, {2}\n", prop.GetType().Name, prop.Name, propValue); + SendSubscriptionCommand(propValue, "1"); + } + + + } + } + catch (Exception e) + { + + Debug.Console(2, "QscDspDialer Subscription Error: '{0}'\n", e); + } + + + // SendSubscriptionCommand(, "1"); + // SendSubscriptionCommand(config. , "mute", 500); + } + public void ParseSubscriptionMessage(string customName, string value) + { + + // Check for valid subscription response + Debug.Console(1, "QscDialerTag {0} Response: '{1}'", customName, value); + if (customName == Tags.dialStringTag) + { + Debug.Console(2, "QscDialerTag DialStringChanged ", value); + this.DialString = value; + this.DialStringFeedback.FireUpdate(); + } + else if (customName == Tags.doNotDisturbTag) + { + if (value == "on") + { + this.DoNotDisturbState = true; + } + else if (value == "off") + { + this.DoNotDisturbState = false; + } + DoNotDisturbFeedback.FireUpdate(); + } + else if (customName == Tags.callStatusTag) + { + if (value == "Dialing") + { + this.OffHook = true; + } + else if (value == "Disconnected") + { + this.OffHook = false; + if (Tags.ClearOnHangup) + { + this.SendKeypad(eKeypadKeys.Clear); + } + } + this.OffHookFeedback.FireUpdate(); + } + else if (customName == Tags.autoAnswerTag) + { + if (value == "on") + { + this.AutoAnswerState = true; + } + else if (value == "off") + { + this.AutoAnswerState = false; + } + AutoAnswerFeedback.FireUpdate(); + } + else if (customName == Tags.hookStatusTag) + { + if (value == "true") + { + this.OffHook = true; + } + else if (value == "false") + { + this.OffHook = false; + } + this.OffHookFeedback.FireUpdate(); + } + + + } + + public void DoNotDisturbToggle() + { + int dndStateInt = !DoNotDisturbState ? 1 : 0; + Parent.SendLine(string.Format("csv {0} {1}", Tags.doNotDisturbTag, dndStateInt)); + } + public void DoNotDisturbOn() + { + Parent.SendLine(string.Format("csv {0} 1", Tags.doNotDisturbTag)); + } + public void DoNotDisturbOff() + { + Parent.SendLine(string.Format("csv {0} 0", Tags.doNotDisturbTag)); + } + public void AutoAnswerToggle() + { + int autoAnswerStateInt = !AutoAnswerState ? 1 : 0; + Parent.SendLine(string.Format("csv {0} {1}", Tags.autoAnswerTag, autoAnswerStateInt)); + } + public void AutoAnswerOn() + { + Parent.SendLine(string.Format("csv {0} 1", Tags.autoAnswerTag)); + } + public void AutoAnswerOff() + { + Parent.SendLine(string.Format("csv {0} 0", Tags.autoAnswerTag)); + } + + private void PollKeypad() + { + Thread.Sleep(50); + Parent.SendLine(string.Format("cg {0}", Tags.dialStringTag)); + } + + public void SendKeypad(eKeypadKeys button) + { + string keypadTag = null; + switch (button) + { + case eKeypadKeys.Num0: keypadTag = Tags.keypad0Tag; break; + case eKeypadKeys.Num1: keypadTag = Tags.keypad1Tag; break; + case eKeypadKeys.Num2: keypadTag = Tags.keypad2Tag; break; + case eKeypadKeys.Num3: keypadTag = Tags.keypad3Tag; break; + case eKeypadKeys.Num4: keypadTag = Tags.keypad4Tag; break; + case eKeypadKeys.Num5: keypadTag = Tags.keypad5Tag; break; + case eKeypadKeys.Num6: keypadTag = Tags.keypad6Tag; break; + case eKeypadKeys.Num7: keypadTag = Tags.keypad7Tag; break; + case eKeypadKeys.Num8: keypadTag = Tags.keypad8Tag; break; + case eKeypadKeys.Num9: keypadTag = Tags.keypad9Tag; break; + case eKeypadKeys.Pound: keypadTag = Tags.keypadPoundTag; break; + case eKeypadKeys.Star: keypadTag = Tags.keypadStarTag; break; + case eKeypadKeys.Backspace: keypadTag = Tags.keypadBackspaceTag; break; + case eKeypadKeys.Clear: keypadTag = Tags.keypadClearTag; break; + } + if (keypadTag != null) + { + var cmdToSend = string.Format("ct {0}", keypadTag); + Parent.SendLine(cmdToSend); + PollKeypad(); + } + } + public void SendSubscriptionCommand(string instanceTag, string changeGroup) + { + // Subscription string format: InstanceTag subscribe attributeCode Index1 customName responseRate + // Ex: "RoomLevel subscribe level 1 MyRoomLevel 500" + + var cmd = string.Format("cga {0} {1}", changeGroup, instanceTag); + + Parent.SendLine(cmd); + } + public void Dial() + { + if (!this.OffHook) + { + Parent.SendLine(string.Format("ct {0}", Tags.connectTag)); + } + else + { + Parent.SendLine(string.Format("ct {0}", Tags.disconnectTag)); + } + Thread.Sleep(50); + Parent.SendLine(string.Format("cg {0}", Tags.callStatusTag)); + } + public void Dial(string number) + { + } + public void EndCall(CodecActiveCallItem activeCall) + { + } + public void EndAllCalls() + { + } + public void AcceptCall(CodecActiveCallItem item) + { + } + + public void RejectCall(CodecActiveCallItem item) + { + + } + + public void SendDtmf(string digit) + { + + } + + public enum eKeypadKeys + { + Num1, + Num2, + Num3, + Num4, + Num5, + Num6, + Num7, + Num8, + Num9, + Num0, + Star, + Pound, + Clear, + Backspace + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspLevelControl.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspLevelControl.cs new file mode 100644 index 00000000..498127f5 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspLevelControl.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System.Text.RegularExpressions; + + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + public class QscDspLevelControl : QscDspControlPoint, IBasicVolumeWithFeedback, IKeyed + { + bool _IsMuted; + ushort _VolumeLevel; + + public BoolFeedback MuteFeedback { get; private set; } + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public bool Enabled { get; set; } + public ePdtLevelTypes Type; + CTimer VolumeUpRepeatTimer; + CTimer VolumeDownRepeatTimer; + + /// + /// Used for to identify level subscription values + /// + public string LevelCustomName { get; private set; } + + /// + /// Used for to identify mute subscription values + /// + public string MuteCustomName { get; private set; } + + /// + /// Minimum fader level + /// + double MinLevel; + + /// + /// Maximum fader level + /// + double MaxLevel; + + /// + /// Checks if a valid subscription string has been recieved for all subscriptions + /// + public bool IsSubsribed + { + get + { + bool isSubscribed = false; + + if (HasMute && MuteIsSubscribed) + isSubscribed = true; + + if (HasLevel && LevelIsSubscribed) + isSubscribed = true; + + return isSubscribed; + } + } + + public bool AutomaticUnmuteOnVolumeUp { get; private set; } + + public bool HasMute { get; private set; } + + public bool HasLevel { get; private set; } + + bool MuteIsSubscribed; + + bool LevelIsSubscribed; + + //public TesiraForteLevelControl(string label, string id, int index1, int index2, bool hasMute, bool hasLevel, BiampTesiraForteDsp parent) + // : base(id, index1, index2, parent) + //{ + // Initialize(label, hasMute, hasLevel); + //} + + public QscDspLevelControl(string key, QscDspLevelControlBlockConfig config, QscDsp parent) + : base(config.LevelInstanceTag, config.MuteInstanceTag, parent) + { + if (!config.Disabled) + { + Initialize(key, config); + } + } + + + /// + /// Initializes this attribute based on config values and generates subscriptions commands and adds commands to the parent's queue. + /// + public void Initialize(string key, QscDspLevelControlBlockConfig config) + { + Key = string.Format("{0}--{1}", Parent.Key, key); + Enabled = true; + DeviceManager.AddDevice(this); + if (config.IsMic) + { + Type = ePdtLevelTypes.microphone; + } + else + { + Type = ePdtLevelTypes.speaker; + } + + Debug.Console(2, this, "Adding LevelControl '{0}'", Key); + + this.IsSubscribed = false; + + MuteFeedback = new BoolFeedback(() => _IsMuted); + + VolumeLevelFeedback = new IntFeedback(() => _VolumeLevel); + + VolumeUpRepeatTimer = new CTimer(VolumeUpRepeat, Timeout.Infinite); + VolumeDownRepeatTimer = new CTimer(VolumeDownRepeat, Timeout.Infinite); + LevelCustomName = config.Label; + HasMute = config.HasMute; + HasLevel = config.HasLevel; + } + + public void Subscribe() + { + // Do subscriptions and blah blah + + // Subscribe to mute + if (this.HasMute) + { + + SendSubscriptionCommand(this.MuteInstanceTag, "1"); + // SendSubscriptionCommand(config. , "mute", 500); + } + + // Subscribe to level + if (this.HasLevel) + { + + SendSubscriptionCommand(this.LevelInstanceTag, "1"); + // SendSubscriptionCommand(this.con, "level", 250); + + //SendFullCommand("get", "minLevel", null); + + //SendFullCommand("get", "maxLevel", null); + } + } + + + /// + /// Parses the response from the DspBase + /// + /// + /// + public void ParseSubscriptionMessage(string customName, string value) + { + + // Check for valid subscription response + Debug.Console(1, this, "Level {0} Response: '{1}'", customName, value); + if (customName == MuteInstanceTag) + { + if (value == "muted") + { + _IsMuted = true; + MuteIsSubscribed = true; + + } + else if (value == "unmuted") + { + _IsMuted = false; + MuteIsSubscribed = true; + } + + MuteFeedback.FireUpdate(); + } + else if (customName == LevelInstanceTag) + { + + + var _value = Double.Parse(value); + + _VolumeLevel = (ushort)(_value * 65535); + Debug.Console(1, this, "Level {0} VolumeLevel: '{1}'", customName, _VolumeLevel); + LevelIsSubscribed = true; + + VolumeLevelFeedback.FireUpdate(); + } + + } + + + /// + /// Turns the mute off + /// + public void MuteOff() + { + SendFullCommand("csv", this.MuteInstanceTag, "0"); + } + + /// + /// Turns the mute on + /// + public void MuteOn() + { + SendFullCommand("csv", this.MuteInstanceTag, "1"); + } + + /// + /// Sets the volume to a specified level + /// + /// + public void SetVolume(ushort level) + { + Debug.Console(1, this, "volume: {0}", level); + // Unmute volume if new level is higher than existing + if (AutomaticUnmuteOnVolumeUp && _IsMuted) + { + MuteOff(); + } + double newLevel = Scale(level); + Debug.Console(1, this, "newVolume: {0}", newLevel); + SendFullCommand("csp", this.LevelInstanceTag, string.Format("{0}", newLevel)); + } + + /// + /// Toggles mute status + /// + public void MuteToggle() + { + + if (_IsMuted) + { + SendFullCommand("csv", this.MuteInstanceTag, "0"); + } + else + { + SendFullCommand("csv", this.MuteInstanceTag, "1"); + } + + } + + public void VolumeUpRepeat(object callbackObject) + { + this.VolumeUp(true); + } + public void VolumeDownRepeat(object callbackObject) + { + this.VolumeDown(true); + } + + public void VolumeDown(bool press) + { + + + if (press) + { + VolumeDownRepeatTimer.Reset(100); + SendFullCommand("css ", this.LevelInstanceTag, "--"); + + + } + else + { + VolumeDownRepeatTimer.Stop(); + // VolumeDownRepeatTimer.Dispose(); + } + } + + /// + /// Increments volume level + /// + /// + public void VolumeUp(bool press) + { + if (press) + { + VolumeUpRepeatTimer.Reset(100); + SendFullCommand("css ", this.LevelInstanceTag, "++"); + + if (AutomaticUnmuteOnVolumeUp) + if (!_IsMuted) + MuteOff(); + } + else + { + VolumeUpRepeatTimer.Stop(); + } + } + /// + double Scale(double input) + { + Debug.Console(1, this, "Scaling (double) input '{0}'",input ); + + var output = (input / 65535); + + Debug.Console(1, this, "Scaled output '{0}'", output); + + return output; + } + } + public enum ePdtLevelTypes + { + speaker = 0, + microphone = 1 + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspPropertiesConfig.cs new file mode 100644 index 00000000..9eab3e84 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DSP/QscDsp/QscDspPropertiesConfig.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.DSP +{ + /// + /// + /// + public class QscDspPropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public ControlPropertiesConfig Control { get; set; } + + /// + /// These are key-value pairs, string id, string type. + /// Valid types are level and mute. + /// Need to include the index values somehow + /// + public Dictionary LevelControlBlocks { get; set; } + public Dictionary dialerControlBlocks { get; set; } + public Dictionary presets { get; set; } + // public Dictionary DialerControlBlocks {get; set;} + } + public interface IQscDspBasicLevel : IBasicVolumeWithFeedback + { + /// + /// In BiAmp: Instance Tag, QSC: Named Control, Polycom: + /// + string LevelInstanceTag { get; set; } + string MuteInstanceTag { get; set; } + bool HasMute { get; } + bool HasLevel { get; } + bool AutomaticUnmuteOnVolumeUp { get; } + } + public class QscDspLevelControlBlockConfig + { + public bool Disabled { get; set; } + public string Label { get; set; } + public string LevelInstanceTag { get; set; } + public string MuteInstanceTag { get; set; } + public bool HasMute { get; set; } + public bool HasLevel { get; set; } + public bool IsMic { get; set; } + } + + public class QscDialerConfig + { + public string incomingCallRingerTag {get; set;} + public string dialStringTag {get; set;} + public string disconnectTag {get; set;} + public string connectTag {get; set;} + public string callStatusTag {get; set;} + public string hookStatusTag {get; set;} + public string doNotDisturbTag { get; set; } + public string autoAnswerTag { get; set; } + + public string keypadBackspaceTag {get; set;} + public string keypadClearTag {get; set;} + public string keypad1Tag {get; set;} + public string keypad2Tag {get; set;} + public string keypad3Tag {get; set;} + public string keypad4Tag {get; set;} + public string keypad5Tag {get; set;} + public string keypad6Tag {get; set;} + public string keypad7Tag {get; set;} + public string keypad8Tag {get; set;} + public string keypad9Tag {get; set;} + public string keypad0Tag {get; set;} + public string keypadPoundTag {get; set;} + public string keypadStarTag {get; set;} + + public bool ClearOnHangup { get; set; } + + } + + public class QscDspPresets + { + public string label { get; set; } + public string preset { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/DiscPlayer/IRDiscPlayerBase.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DiscPlayer/IRDiscPlayerBase.cs new file mode 100644 index 00000000..50190294 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/DiscPlayer/IRDiscPlayerBase.cs @@ -0,0 +1,314 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Common +{ + public class IRBlurayBase : Device, IDiscPlayerControls, IUiDisplayInfo, IRoutingOutputs, IUsageTracking + { + public IrOutputPortController IrPort { get; private set; } + + public uint DisplayUiType { get { return DisplayUiConstants.TypeBluray; } } + + public IRBlurayBase(string key, string name, IrOutputPortController portCont) + : base(key, name) + { + IrPort = portCont; + DeviceManager.AddDevice(portCont); + + HasKeypadAccessoryButton1 = true; + KeypadAccessoryButton1Command = "Clear"; + KeypadAccessoryButton1Label = "Clear"; + + HasKeypadAccessoryButton2 = true; + KeypadAccessoryButton2Command = "NumericEnter"; + KeypadAccessoryButton2Label = "Enter"; + + PowerIsOnFeedback = new BoolFeedback(() => _PowerIsOn); + + HdmiOut = new RoutingOutputPort(RoutingPortNames.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + AnyAudioOut = new RoutingOutputPort(RoutingPortNames.AnyAudioOut, eRoutingSignalType.Audio, + eRoutingPortConnectionType.DigitalAudio, null, this); + OutputPorts = new RoutingPortCollection { HdmiOut, AnyAudioOut }; + } + + + #region IDPad Members + + public void Up(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_UP_ARROW, pressRelease); + } + + public void Down(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_DN_ARROW, pressRelease); + } + + public void Left(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_LEFT_ARROW, pressRelease); + } + + public void Right(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_RIGHT_ARROW, pressRelease); + } + + public void Select(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_ENTER, pressRelease); + } + + public void Menu(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_MENU, pressRelease); + } + + public void Exit(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_EXIT, pressRelease); + } + + #endregion + + #region INumericKeypad Members + + public void Digit0(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_0, pressRelease); + } + + public void Digit1(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_1, pressRelease); + } + + public void Digit2(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_2, pressRelease); + } + + public void Digit3(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_3, pressRelease); + } + + public void Digit4(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_4, pressRelease); + } + + public void Digit5(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_5, pressRelease); + } + + public void Digit6(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_6, pressRelease); + } + + public void Digit7(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_7, pressRelease); + } + + public void Digit8(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_8, pressRelease); + } + + public void Digit9(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_9, pressRelease); + } + + /// + /// Defaults to true + /// + public bool HasKeypadAccessoryButton1 { get; set; } + + /// + /// Defaults to "-" + /// + public string KeypadAccessoryButton1Label { get; set; } + + + /// + /// Defaults to "Dash" + /// + public string KeypadAccessoryButton1Command { get; set; } + + public void KeypadAccessoryButton1(bool pressRelease) + { + IrPort.PressRelease(KeypadAccessoryButton1Command, pressRelease); + } + + /// + /// Defaults to true + /// + public bool HasKeypadAccessoryButton2 { get; set; } + + /// + /// Defaults to "Enter" + /// + public string KeypadAccessoryButton2Label { get; set; } + + + /// + /// Defaults to "Enter" + /// + public string KeypadAccessoryButton2Command { get; set; } + + public void KeypadAccessoryButton2(bool pressRelease) + { + IrPort.PressRelease(KeypadAccessoryButton2Command, pressRelease); + } + + #endregion + + #region IChannelFunctions Members + + public void ChannelUp(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_CH_PLUS, pressRelease); + } + + public void ChannelDown(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_CH_MINUS, pressRelease); + } + + public void LastChannel(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_LAST, pressRelease); + } + + public void Guide(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_GUIDE, pressRelease); + } + + public void Info(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_INFO, pressRelease); + } + + #endregion + + #region IColorFunctions Members + + public void Red(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_RED, pressRelease); + } + + public void Green(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_GREEN, pressRelease); + } + + public void Yellow(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_YELLOW, pressRelease); + } + + public void Blue(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_BLUE, pressRelease); + } + + #endregion + + #region IRoutingOutputs Members + + public RoutingOutputPort HdmiOut { get; private set; } + public RoutingOutputPort AnyAudioOut { get; private set; } + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + #region IPower Members + + public void PowerOn() + { + IrPort.Pulse(IROutputStandardCommands.IROut_POWER_ON, 200); + _PowerIsOn = true; + } + + public void PowerOff() + { + IrPort.Pulse(IROutputStandardCommands.IROut_POWER_OFF, 200); + _PowerIsOn = false; + } + + public void PowerToggle() + { + IrPort.Pulse(IROutputStandardCommands.IROut_POWER, 200); + _PowerIsOn = false; + } + + public BoolFeedback PowerIsOnFeedback { get; set; } + bool _PowerIsOn; + + #endregion + + #region ITransport Members + + public void Play(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_PLAY, pressRelease); + } + + public void Pause(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_PAUSE, pressRelease); + } + + public void Rewind(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_RSCAN, pressRelease); + } + + public void FFwd(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_FSCAN, pressRelease); + } + + public void ChapMinus(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_TRACK_MINUS, pressRelease); + } + + public void ChapPlus(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_TRACK_PLUS, pressRelease); + } + + public void Stop(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_STOP, pressRelease); + } + + public void Record(bool pressRelease) + { + } + + #endregion + + #region IUsageTracking Members + + public UsageTracking UsageTracker { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/AvocorVTFDisplay.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/AvocorVTFDisplay.cs new file mode 100644 index 00000000..7b11ed33 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/AvocorVTFDisplay.cs @@ -0,0 +1,720 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.CrestronThread; +using Crestron.SimplSharpPro; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Displays +{ + /// + /// + /// + public class AvocorDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, ICommunicationMonitor, IInputDisplayPort1, + IInputHdmi1, IInputHdmi2, IInputHdmi3, IInputHdmi4, IInputVga1 + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + public byte ID { get; private set; } + + /// + /// 0x08 + /// + const byte InputVga1Value = 0x00; + /// + /// 0x09 + /// + const byte InputHdmi1Value = 0x09; + /// + /// 0x0a + /// + const byte InputHdmi2Value = 0x0a; + /// + /// 0x0b + /// + const byte InputHdmi3Value = 0x0b; + /// + /// 0x0c + /// + const byte InputHdmi4Value = 0x0c; + /// + /// 0c0d + /// + const byte InputDisplayPort1Value = 0x0d; + /// + /// 0x0e + /// + const byte InputIpcOpsValue = 0x0e; + /// + /// 0x11 + /// + const byte InputHdmi5Value = 0x11; + /// + /// 0x12 + /// + const byte InputMediaPlayerValue = 0x12; + + + bool _PowerIsOn; + bool _IsWarmingUp; + bool _IsCoolingDown; + ushort _VolumeLevelForSig; + int _LastVolumeSent; + ushort _PreMuteVolumeLevel; + bool _IsMuted; + RoutingInputPort _CurrentInputPort; + ActionIncrementer VolumeIncrementer; + bool VolumeIsRamping; + public bool IsInStandby { get; private set; } + + protected override Func PowerIsOnFeedbackFunc { get { return () => _PowerIsOn; } } + protected override Func IsCoolingDownFeedbackFunc { get { return () => _IsCoolingDown; } } + protected override Func IsWarmingUpFeedbackFunc { get { return () => _IsWarmingUp; } } + protected override Func CurrentInputFeedbackFunc { get { return () => _CurrentInputPort.Key; } } + + /// + /// Constructor for IBasicCommunication + /// + public AvocorDisplay(string key, string name, IBasicCommunication comm, string id) + : base(key, name) + { + Communication = comm; + //Communication.BytesReceived += new EventHandler(Communication_BytesReceived); + + PortGather = new CommunicationGather(Communication, '\x08'); + PortGather.IncludeDelimiter = true; + PortGather.LineReceived += new EventHandler(PortGather_LineReceived); + + ID = id == null ? (byte)0x01 : Convert.ToByte(id, 16); // If id is null, set default value of 0x01, otherwise assign value passed in constructor + Init(); + } + + /// + /// Constructor for TCP + /// + public AvocorDisplay(string key, string name, string hostname, int port, string id) + : base(key, name) + { + Communication = new GenericTcpIpClient(key + "-tcp", hostname, port, 5000); + + PortGather = new CommunicationGather(Communication, '\x0d'); + PortGather.IncludeDelimiter = true; + PortGather.LineReceived += new EventHandler(PortGather_LineReceived); + + ID = id == null ? (byte)0x01 : Convert.ToByte(id, 16); // If id is null, set default value of 0x01, otherwise assign value passed in constructor + Init(); + } + + /// + /// Constructor for COM + /// + public AvocorDisplay(string key, string name, ComPort port, ComPort.ComPortSpec spec, string id) + : base(key, name) + { + Communication = new ComPortController(key + "-com", port, spec); + + PortGather = new CommunicationGather(Communication, '\x0d'); + PortGather.IncludeDelimiter = true; + PortGather.LineReceived += new EventHandler(PortGather_LineReceived); + + ID = id == null ? (byte)0x01 : Convert.ToByte(id, 16); // If id is null, set default value of 0x01, otherwise assign value passed in constructor + Init(); + } + + void AddRoutingInputPort(RoutingInputPort port, byte fbMatch) + { + port.FeedbackMatchObject = fbMatch; + InputPorts.Add(port); + } + + void Init() + { + WarmupTime = 10000; + CooldownTime = 8000; + + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 30000, 120000, 300000, StatusGet); + DeviceManager.AddDevice(CommunicationMonitor); + + VolumeIncrementer = new ActionIncrementer(655, 0, 65535, 800, 80, + v => SetVolume((ushort)v), + () => _LastVolumeSent); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi1), this), InputHdmi1Value); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi2), this), InputHdmi2Value); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi3), this), InputHdmi3Value); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn4, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi4), this), InputHdmi4Value); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn5, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi5), this), InputHdmi5Value); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.DisplayPortIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DisplayPort, new Action(InputDisplayPort1), this), InputDisplayPort1Value); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.VgaIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Dvi, new Action(InputVga1), this), InputVga1Value); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.IpcOps, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Composite, new Action(InputIpcOps), this), InputIpcOpsValue); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.MediaPlayer, eRoutingSignalType.Video, + eRoutingPortConnectionType.Vga, new Action(InputMediaPlayer), this), InputMediaPlayerValue); + + VolumeLevelFeedback = new IntFeedback(() => { return _VolumeLevelForSig; }); + MuteFeedback = new BoolFeedback(() => _IsMuted); + } + + /// + /// + /// + /// + public override bool CustomActivate() + { + Communication.Connect(); + + var socket = Communication as ISocketStatus; + if (socket != null) + { + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + StatusGet(); + + return true; + } + + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + if (e.Client.IsConnected) + StatusGet(); + } + + public override FeedbackCollection Feedbacks + { + get + { + var list = base.Feedbacks; + list.AddRange(new List + { + VolumeLevelFeedback, + MuteFeedback, + CurrentInputFeedback + }); + return list; + } + } + + ///// + ///// / + ///// + ///// + //void Communication_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e) + //{ + // // This is probably not thread-safe buffering + // // Append the incoming bytes with whatever is in the buffer + // var newBytes = new byte[IncomingBuffer.Length + e.Bytes.Length]; + // IncomingBuffer.CopyTo(newBytes, 0); + // e.Bytes.CopyTo(newBytes, IncomingBuffer.Length); + + // if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 + // Debug.Console(2, this, "Received:{0}", ComTextHelper.GetEscapedText(newBytes)); + + // // Need to find AA FF and have + // for (int i = 0; i < newBytes.Length; i++) + // { + // if (newBytes[i] == 0xAA && newBytes[i + 1] == 0xFF) + // { + // newBytes = newBytes.Skip(i).ToArray(); // Trim off junk if there's "dirt" in the buffer + + // // parse it + // // If it's at least got the header, then process it, + // while (newBytes.Length > 4 && newBytes[0] == 0xAA && newBytes[1] == 0xFF) + // { + // var msgLen = newBytes[3]; + // // if the buffer is shorter than the header (3) + message (msgLen) + checksum (1), + // // give and save it for next time + // if (newBytes.Length < msgLen + 4) + // break; + + // // Good length, grab the message + // var message = newBytes.Skip(4).Take(msgLen).ToArray(); + + // // At this point, the ack/nak is the first byte + // if (message[0] == 0x41) + // { + // switch (message[1]) // type byte + // { + // case 0x00: // General status + // UpdatePowerFB(message[2], message[5]); // "power" can be misrepresented when the display sleeps + // UpdateInputFb(message[5]); + // UpdateVolumeFB(message[3]); + // UpdateMuteFb(message[4]); + // UpdateInputFb(message[5]); + // break; + + // case 0x11: + // UpdatePowerFB(message[2]); + // break; + + // case 0x12: + // UpdateVolumeFB(message[2]); + // break; + + // case 0x13: + // UpdateMuteFb(message[2]); + // break; + + // case 0x14: + // UpdateInputFb(message[2]); + // break; + + // default: + // break; + // } + // } + // // Skip over what we've used and save the rest for next time + // newBytes = newBytes.Skip(5 + msgLen).ToArray(); + // } + + // break; // parsing will mean we can stop looking for header in loop + // } + // } + + // // Save whatever partial message is here + // IncomingBuffer = newBytes; + //} + + void PortGather_LineReceived(object sender, GenericCommMethodReceiveTextArgs e) + { + Debug.Console(1, this, "Receivied: '{0}'", ComTextHelper.GetEscapedText(e.Text)); + + if (e.Text.IndexOf("\x50\x4F\x57") > -1) + { + // Power Status Response + + var value = e.Text.ToCharArray(); + + switch (value[6]) + { + case '\x00': + { + _PowerIsOn = false; + break; + } + case '\x01': + { + _PowerIsOn = true; + break; + } + } + + PowerIsOnFeedback.FireUpdate(); + Debug.Console(1, this, "PowerIsOn State: {0}", PowerIsOnFeedback.BoolValue); + + } + else if (e.Text.IndexOf("\x4D\x49\x4E") > -1) + { + var value = e.Text.ToCharArray(); + + var b = value[6]; + + var newInput = InputPorts.FirstOrDefault(i => i.FeedbackMatchObject.Equals(b)); + if (newInput != null && newInput != _CurrentInputPort) + { + _CurrentInputPort = newInput; + CurrentInputFeedback.FireUpdate(); + Debug.Console(1, this, "Current Input: {0}", CurrentInputFeedback.StringValue); + } + + } + else if (e.Text.IndexOf("\x56\x4F\x4C") > -1) + { + // Volume Status Response + + var value = e.Text.ToCharArray(); + + var b = value[6]; + + var newVol = (ushort)NumericalHelpers.Scale((double)b, 0, 100, 0, 65535); + if (!VolumeIsRamping) + _LastVolumeSent = newVol; + if (newVol != _VolumeLevelForSig) + { + _VolumeLevelForSig = newVol; + VolumeLevelFeedback.FireUpdate(); + + if (_VolumeLevelForSig > 0) + _IsMuted = false; + else + _IsMuted = true; + + MuteFeedback.FireUpdate(); + + Debug.Console(1, this, "Volume Level: {0}", VolumeLevelFeedback.IntValue); + } + } + + } + + /// + /// + /// + void UpdatePowerFB(byte powerByte) + { + var newVal = powerByte == 1; + if (newVal != _PowerIsOn) + { + _PowerIsOn = newVal; + PowerIsOnFeedback.FireUpdate(); + } + } + + /// + /// Updates power status from general updates where source is included. + /// Compensates for errant standby / power off hiccups by ignoring + /// power off states with input < 0x10 + /// + void UpdatePowerFB(byte powerByte, byte inputByte) + { + // This should reject errant power feedbacks when switching away from input on standby. + if (powerByte == 0x01 && inputByte < 0x10) + IsInStandby = true; + if (powerByte == 0x00 && IsInStandby) // Ignore power off if coming from standby - glitch + { + IsInStandby = false; + return; + } + + UpdatePowerFB(powerByte); + } + + /// + /// + /// + void UpdateVolumeFB(byte b) + { + var newVol = (ushort)NumericalHelpers.Scale((double)b, 0, 100, 0, 65535); + if (!VolumeIsRamping) + _LastVolumeSent = newVol; + if (newVol != _VolumeLevelForSig) + { + _VolumeLevelForSig = newVol; + VolumeLevelFeedback.FireUpdate(); + } + } + + /// + /// + /// + void UpdateMuteFb(byte b) + { + var newMute = b == 1; + if (newMute != _IsMuted) + { + _IsMuted = newMute; + MuteFeedback.FireUpdate(); + } + } + + /// + /// + /// + void UpdateInputFb(byte b) + { + var newInput = InputPorts.FirstOrDefault(i => i.FeedbackMatchObject.Equals(b)); + if (newInput != null && newInput != _CurrentInputPort) + { + _CurrentInputPort = newInput; + CurrentInputFeedback.FireUpdate(); + } + } + + /// + /// Formats an outgoing message. Replaces third byte with ID and replaces last byte with checksum + /// + /// + void SendBytes(byte[] b) + { + b[1] = ID; + + var command = b.ToString(); + + Debug.Console(1, this, "Sending: '{0}'",ComTextHelper.GetEscapedText(b)); + + Communication.SendBytes(b); + } + + + /// + /// + /// + public void StatusGet() + { + PowerGet(); + + CrestronEnvironment.Sleep(100); + + InputGet(); + + CrestronEnvironment.Sleep(100); + + VolumeGet(); + } + + /// + /// + /// + public override void PowerOn() + { + //Send(PowerOnCmd); + SendBytes(new byte[] { 0x07, ID, 0x02, 0x50, 0x4F, 0x57, 0x01, 0x08, 0x0d }); + if (!PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) + { + _IsWarmingUp = true; + IsWarmingUpFeedback.FireUpdate(); + // Fake power-up cycle + WarmupTimer = new CTimer(o => + { + _IsWarmingUp = false; + _PowerIsOn = true; + IsWarmingUpFeedback.FireUpdate(); + PowerIsOnFeedback.FireUpdate(); + }, WarmupTime); + } + } + + /// + /// + /// + public override void PowerOff() + { + // If a display has unreliable-power off feedback, just override this and + // remove this check. + if (!_IsWarmingUp && !_IsCoolingDown) // PowerIsOnFeedback.BoolValue && + { + //Send(PowerOffCmd); + SendBytes(new byte[] { 0x07, ID, 0x02, 0x50, 0x4F, 0x57, 0x00, 0x08, 0x0d }); + _IsCoolingDown = true; + _PowerIsOn = false; + PowerIsOnFeedback.FireUpdate(); + IsCoolingDownFeedback.FireUpdate(); + // Fake cool-down cycle + CooldownTimer = new CTimer(o => + { + _IsCoolingDown = false; + IsCoolingDownFeedback.FireUpdate(); + }, CooldownTime); + } + } + + public override void PowerToggle() + { + if (PowerIsOnFeedback.BoolValue && !IsWarmingUpFeedback.BoolValue) + PowerOff(); + else if (!PowerIsOnFeedback.BoolValue && !IsCoolingDownFeedback.BoolValue) + PowerOn(); + } + + public void PowerGet() + { + SendBytes(new byte[] { 0x07, ID, 0x01, 0x50, 0x4F, 0x57, 0x08, 0x0d }); + } + + public void InputHdmi1() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, InputHdmi1Value, 0x08, 0x0d }); + } + + public void InputHdmi2() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, InputHdmi2Value, 0x08, 0x0d }); + } + + public void InputHdmi3() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, InputHdmi3Value, 0x08, 0x0d }); + } + + public void InputHdmi4() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, InputHdmi4Value, 0x08, 0x0d }); + } + + public void InputHdmi5() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, InputHdmi5Value, 0x08, 0x0d }); + } + + public void InputDisplayPort1() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, InputDisplayPort1Value, 0x08, 0x0d }); + } + + public void InputVga1() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, InputVga1Value, 0x08, 0x0d }); + } + + public void InputIpcOps() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, InputIpcOpsValue, 0x08, 0x0d }); + } + + public void InputMediaPlayer() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, InputMediaPlayerValue, 0x08, 0x0d }); + } + + public void InputGet() + { + SendBytes(new byte[] { 0x07, ID, 0x01, 0x4D, 0x49, 0x4E, 0x08, 0x0d }); + } + + public void VolumeGet() + { + SendBytes(new byte[] { 0x07, ID, 0x01, 0x56, 0x4F, 0x4C, 0x08, 0x0d }); + } + + + /// + /// Executes a switch, turning on display if necessary. + /// + /// + public override void ExecuteSwitch(object selector) + { + //if (!(selector is Action)) + // Debug.Console(1, this, "WARNING: ExecuteSwitch cannot handle type {0}", selector.GetType()); + + if (_PowerIsOn) + (selector as Action)(); + else // if power is off, wait until we get on FB to send it. + { + // One-time event handler to wait for power on before executing switch + EventHandler handler = null; // necessary to allow reference inside lambda to handler + handler = (o, a) => + { + if (!_IsWarmingUp) // Done warming + { + IsWarmingUpFeedback.OutputChange -= handler; + (selector as Action)(); + } + }; + IsWarmingUpFeedback.OutputChange += handler; // attach and wait for on FB + PowerOn(); + } + } + + /// + /// Scales the level to the range of the display and sends the command + /// + /// + public void SetVolume(ushort level) + { + _LastVolumeSent = level; + var scaled = (int)NumericalHelpers.Scale(level, 0, 65535, 0, 100); + // The inputs to Scale ensure that byte won't overflow + SendBytes(new byte[] { 0x07, ID, 0x02, 0x56, 0x4F, 0x4C, Convert.ToByte(scaled), 0x08, 0x0d }); + } + + #region IBasicVolumeWithFeedback Members + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + /// + /// + /// + public void MuteOff() + { + SetVolume(_PreMuteVolumeLevel); + } + + /// + /// + /// + public void MuteOn() + { + _PreMuteVolumeLevel = _VolumeLevelForSig; + + SetVolume(0); + } + + ///// + ///// + ///// + //public void MuteGet() + //{ + // SendBytes(new byte[] { 0x07, ID, 0x01, }); + //} + + #endregion + + #region IBasicVolumeControls Members + + /// + /// + /// + public void MuteToggle() + { + if (_IsMuted) + MuteOff(); + else + MuteOn(); + } + + /// + /// + /// + /// + public void VolumeDown(bool pressRelease) + { + if (pressRelease) + { + VolumeIncrementer.StartDown(); + VolumeIsRamping = true; + } + else + { + VolumeIsRamping = false; + VolumeIncrementer.Stop(); + } + } + + /// + /// + /// + /// + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + { + VolumeIncrementer.StartUp(); + VolumeIsRamping = true; + } + else + { + VolumeIsRamping = false; + VolumeIncrementer.Stop(); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/ComTcpDisplayBase.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/ComTcpDisplayBase.cs new file mode 100644 index 00000000..da92df1f --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/ComTcpDisplayBase.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using PepperDash.Core; +using PepperDash.Essentials.Core; + + +namespace PepperDash.Essentials.Devices.Displays +{ + public abstract class ComTcpDisplayBase : DisplayBase, IPower + { + /// + /// Sets the communication method for this - swaps out event handlers and output handlers + /// + public IBasicCommunication CommunicationMethod + { + get { return _CommunicationMethod; } + set + { + if (_CommunicationMethod != null) + _CommunicationMethod.BytesReceived -= this.CommunicationMethod_BytesReceived; + // Outputs??? + _CommunicationMethod = value; + if (_CommunicationMethod != null) + _CommunicationMethod.BytesReceived += this.CommunicationMethod_BytesReceived; + // Outputs? + } + } + IBasicCommunication _CommunicationMethod; + + public ComTcpDisplayBase(string key, string name) + : base(key, name) + { + + } + + + protected abstract void CommunicationMethod_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs args); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/DeviceFactory.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/DeviceFactory.cs new file mode 100644 index 00000000..8b4941d7 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/DeviceFactory.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharpPro; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.Devices.Displays +{ + public class DisplayDeviceFactory + { + public static IKeyed GetDevice(DeviceConfig dc) + { + var key = dc.Key; + var name = dc.Name; + var type = dc.Type; + var properties = dc.Properties; + + var typeName = dc.Type.ToLower(); + + try + { + if (typeName == "necmpsx") + { + var comm = CommFactory.CreateCommForDevice(dc); + if (comm != null) + return new NecPSXMDisplay(dc.Key, dc.Name, comm); + } + if (typeName == "panasonicthef") + { + var comm = CommFactory.CreateCommForDevice(dc); + if (comm != null) + return new PanasonicThefDisplay(dc.Key, dc.Name, comm); + } + else if(typeName == "samsungmdc") + { + var comm = CommFactory.CreateCommForDevice(dc); + if (comm != null) + return new SamsungMDC(dc.Key, dc.Name, comm, dc.Properties["id"].Value()); + } + if (typeName == "avocorvtf") + { + var comm = CommFactory.CreateCommForDevice(dc); + if (comm != null) + return new AvocorDisplay(dc.Key, dc.Name, comm, null); + } + + } + catch (Exception e) + { + Debug.Console(0, "Displays factory: Exception creating device type {0}, key {1}: \nCONFIG JSON: {2} \nERROR: {3}\n\n", + dc.Type, dc.Key, JsonConvert.SerializeObject(dc), e); + return null; + } + + return null; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/InputInterfaces.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/InputInterfaces.cs new file mode 100644 index 00000000..ce7113e1 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/InputInterfaces.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Displays +{ + public interface IInputHdmi1 { void InputHdmi1(); } + public interface IInputHdmi2 { void InputHdmi2(); } + public interface IInputHdmi3 { void InputHdmi3(); } + public interface IInputHdmi4 { void InputHdmi4(); } + public interface IInputDisplayPort1 { void InputDisplayPort1(); } + public interface IInputDisplayPort2 { void InputDisplayPort2(); } + public interface IInputVga1 { void InputVga1(); } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/NECPSXMDisplay.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/NECPSXMDisplay.cs new file mode 100644 index 00000000..5ff419c3 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/NECPSXMDisplay.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Displays +{ + /// + /// + /// + public class NecPSXMDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, ICommunicationMonitor + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + + #region Command constants + public const string InputGetCmd = "\x01\x30\x41\x30\x43\x30\x36\x02\x30\x30\x36\x30\x03\x03\x0D"; + public const string Hdmi1Cmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x30\x30\x30\x31\x31\x03\x72\x0d"; + public const string Hdmi2Cmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x30\x30\x30\x31\x32\x03\x71\x0D"; + public const string Hdmi3Cmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x30\x30\x30\x38\x32\x03\x78\x0D"; + public const string Hdmi4Cmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x30\x30\x30\x38\x33\x03\x79\x0D"; + public const string Dp1Cmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x30\x30\x30\x30\x46\x03\x04\x0D"; + public const string Dp2Cmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x30\x30\x30\x31\x30\x03\x73\x0D"; + public const string Dvi1Cmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x30\x30\x30\x30\x33\x03\x71\x0d"; + public const string Video1Cmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x30\x30\x30\x30\x35\x03\x77\x0D"; + public const string VgaCmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x30\x30\x30\x30\x31\x03\x73\x0D"; + public const string RgbCmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x30\x30\x30\x30\x32\x03\x70\x0D"; + + public const string PowerOnCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x30\x33\x44\x36\x30\x30\x30\x31\x03\x73\x0D"; + public const string PowerOffCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x30\x33\x44\x36\x30\x30\x30\x34\x03\x76\x0D"; + public const string PowerToggleIrCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x31\x30\x30\x30\x30\x33\x30\x33\x03\x02\x0D"; + + public const string MuteOffCmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x38\x44\x30\x30\x30\x30\x03\x08\x0D"; + public const string MuteOnCmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x38\x44\x30\x30\x30\x31\x03\x09\x0D"; + public const string MuteToggleIrCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x31\x30\x30\x30\x31\x42\x30\x33\x03\x72\x0D"; + public const string MuteGetCmd = "\x01\x30\x41\x30\x43\x30\x36\x02\x30\x30\x38\x44\x03\x79\x0D"; + + public const string VolumeGetCmd = "\x01\x30\x41\x30\x43\x30\x36\x02\x30\x30\x36\x32\x03\x01\x0D"; + public const string VolumeLevelPartialCmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x30\x30\x36\x32"; //\x46\x46\x46\x46\x03\xNN\x0D + public const string VolumeUpCmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x31\x30\x41\x44\x30\x30\x30\x31\x03\x71\x0D"; + public const string VolumeDownCmd = "\x01\x30\x41\x30\x45\x30\x41\x02\x31\x30\x41\x44\x30\x30\x30\x32\x03\x72\x0D"; + + public const string MenuIrCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x31\x30\x30\x30\x32\x30\x30\x33\x03\x03\x0D"; + public const string UpIrCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x31\x30\x30\x30\x31\x35\x30\x33\x03\x05\x0D"; + public const string DownIrCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x31\x30\x30\x30\x31\x34\x30\x33\x03\x04\x0D"; + public const string LeftIrCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x31\x30\x30\x30\x32\x31\x30\x33\x03\x02\x0D"; + public const string RightIrCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x31\x30\x30\x30\x32\x32\x30\x33\x03\x01\x0D"; + public const string SelectIrCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x31\x30\x30\x30\x32\x33\x30\x33\x03\x00\x0D"; + public const string ExitIrCmd = "\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x31\x30\x30\x30\x31\x46\x30\x33\x03\x76\x0D"; + #endregion + + bool _PowerIsOn; + bool _IsWarmingUp; + bool _IsCoolingDown; + ushort _VolumeLevel; + bool _IsMuted; + + protected override Func PowerIsOnFeedbackFunc { get { return () => _PowerIsOn; } } + protected override Func IsCoolingDownFeedbackFunc { get { return () => _IsCoolingDown; } } + protected override Func IsWarmingUpFeedbackFunc { get { return () => _IsWarmingUp; } } + protected override Func CurrentInputFeedbackFunc { get { return () => "Not Implemented"; } } + + + /// + /// Constructor for IBasicCommunication + /// + public NecPSXMDisplay(string key, string name, IBasicCommunication comm) + : base(key, name) + { + Communication = comm; + Init(); + } + /// + /// Constructor for TCP + /// + public NecPSXMDisplay(string key, string name, string hostname, int port) + : base(key, name) + { + Communication = new GenericTcpIpClient(key + "-tcp", hostname, port, 5000); + Init(); + } + + + /// + /// Constructor for COM + /// + public NecPSXMDisplay(string key, string name, ComPort port, ComPort.ComPortSpec spec) + : base(key, name) + { + Communication = new ComPortController(key + "-com", port, spec); + Init(); + } + + void Init() + { + PortGather = new CommunicationGather(Communication, '\x0d'); + PortGather.LineReceived += this.Port_LineReceived; + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 30000, 120000, 300000, "xx\x0d"); + + InputPorts.Add(new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi1), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi2), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi3), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.HdmiIn4, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi4), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.DisplayPortIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DisplayPort, new Action(InputDisplayPort1), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.DisplayPortIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DisplayPort, new Action(InputDisplayPort2), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.DviIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Dvi, new Action(InputDvi1), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.CompositeIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Composite, new Action(InputVideo1), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.VgaIn, eRoutingSignalType.Video, + eRoutingPortConnectionType.Vga, new Action(InputVga), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.RgbIn, eRoutingSignalType.Video, + eRoutingPortConnectionType.Rgb, new Action(new Action(InputRgb)), this)); + + VolumeLevelFeedback = new IntFeedback(() => { return _VolumeLevel; }); + MuteFeedback = new BoolFeedback(() => _IsMuted); + + // new BoolCueActionPair(CommonBoolCue.Menu, b => { if(b) Send(MenuIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Up, b => { if(b) Send(UpIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Down, b => { if(b) Send(DownIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Left, b => { if(b) Send(LeftIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Right, b => { if(b) Send(RightIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Select, b => { if(b) Send(SelectIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Exit, b => { if(b) Send(ExitIrCmd); }), + //}; + } + + ~NecPSXMDisplay() + { + PortGather = null; + } + + public override bool CustomActivate() + { + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + return true; + } + + public override FeedbackCollection Feedbacks + { + get + { + var list = base.Feedbacks; + list.AddRange(new List + { + + }); + return list; + } + } + + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + if (Debug.Level == 2) + Debug.Console(2, this, "Received: '{0}'", ComTextHelper.GetEscapedText(args.Text)); + + if (args.Text=="DO SOMETHING HERE EVENTUALLY") + { + _IsMuted = true; + MuteFeedback.FireUpdate(); + } + } + + void AppendChecksumAndSend(string s) + { + int x = 0; + for (int i = 1; i < s.Length; i++) + x = x ^ s[i]; + + string send = s + (char)x + '\x0d'; + Send(send); + } + + void Send(string s) + { + if (Debug.Level == 2) + Debug.Console(2, this, "Send: '{0}'", ComTextHelper.GetEscapedText(s)); + Communication.SendText(s); + } + + + public override void PowerOn() + { + Send(PowerOnCmd); + if (!PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) + { + _IsWarmingUp = true; + IsWarmingUpFeedback.FireUpdate(); + // Fake power-up cycle + WarmupTimer = new CTimer(o => + { + _IsWarmingUp = false; + _PowerIsOn = true; + IsWarmingUpFeedback.FireUpdate(); + PowerIsOnFeedback.FireUpdate(); + }, WarmupTime); + } + } + + public override void PowerOff() + { + // If a display has unreliable-power off feedback, just override this and + // remove this check. + if (PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) + { + Send(PowerOffCmd); + _IsCoolingDown = true; + _PowerIsOn = false; + PowerIsOnFeedback.FireUpdate(); + IsCoolingDownFeedback.FireUpdate(); + // Fake cool-down cycle + CooldownTimer = new CTimer(o => + { + Debug.Console(2, this, "Cooldown timer ending"); + _IsCoolingDown = false; + IsCoolingDownFeedback.FireUpdate(); + }, CooldownTime); + } + } + + public override void PowerToggle() + { + if (PowerIsOnFeedback.BoolValue && !IsWarmingUpFeedback.BoolValue) + PowerOff(); + else if (!PowerIsOnFeedback.BoolValue && !IsCoolingDownFeedback.BoolValue) + PowerOn(); + } + + public void InputHdmi1() + { + Send(Hdmi1Cmd); + } + + public void InputHdmi2() + { + Send(Hdmi2Cmd); + } + + public void InputHdmi3() + { + Send(Hdmi3Cmd); + } + + public void InputHdmi4() + { + Send(Hdmi4Cmd); + } + + public void InputDisplayPort1() + { + Send(Dp1Cmd); + } + + public void InputDisplayPort2() + { + Send(Dp2Cmd); + } + + public void InputDvi1() + { + Send(Dvi1Cmd); + } + + public void InputVideo1() + { + Send(Video1Cmd); + } + + public void InputVga() + { + Send(VgaCmd); + } + + public void InputRgb() + { + Send(RgbCmd); + } + + public override void ExecuteSwitch(object selector) + { + if (selector is Action) + (selector as Action).Invoke(); + else + Debug.Console(1, this, "WARNING: ExecuteSwitch cannot handle type {0}", selector.GetType()); + //Send((string)selector); + } + + void SetVolume(ushort level) + { + var levelString = string.Format("{0}{1:X4}\x03", VolumeLevelPartialCmd, level); + AppendChecksumAndSend(levelString); + //Debug.Console(2, this, "Volume:{0}", ComTextHelper.GetEscapedText(levelString)); + _VolumeLevel = level; + VolumeLevelFeedback.FireUpdate(); + } + + #region IBasicVolumeWithFeedback Members + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + public void MuteOff() + { + Send(MuteOffCmd); + } + + public void MuteOn() + { + Send(MuteOnCmd); + } + + void IBasicVolumeWithFeedback.SetVolume(ushort level) + { + SetVolume(level); + } + + #endregion + + #region IBasicVolumeControls Members + + public void MuteToggle() + { + Send(MuteToggleIrCmd); + } + + public void VolumeDown(bool pressRelease) + { + //throw new NotImplementedException(); +//#warning need incrementer for these + SetVolume(_VolumeLevel++); + } + + public void VolumeUp(bool pressRelease) + { + //throw new NotImplementedException(); + SetVolume(_VolumeLevel--); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/NecPaSeriesProjector.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/NecPaSeriesProjector.cs new file mode 100644 index 00000000..7888bace --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/NecPaSeriesProjector.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Essentials.Core; +using PepperDash.Core; + +namespace PepperDash.Essentials.Devices.Displays +{ + public class NecPaSeriesProjector : ComTcpDisplayBase + { + public readonly IntFeedback Lamp1RemainingPercent; + int _Lamp1RemainingPercent; + public readonly IntFeedback Lamp2RemainingPercent; + int _Lamp2RemainingPercent; + protected override Func PowerIsOnFeedbackFunc + { + get { return () => _PowerIsOn; } + } + bool _PowerIsOn; + + protected override Func IsCoolingDownFeedbackFunc + { + get { return () => false; } + } + + protected override Func IsWarmingUpFeedbackFunc + { + get { return () => false; } + } + + public override void PowerToggle() + { + throw new NotImplementedException(); + } + + public override void ExecuteSwitch(object selector) + { + throw new NotImplementedException(); + } + + Dictionary InputMap; + + /// + /// Constructor + /// + public NecPaSeriesProjector(string key, string name) + : base(key, name) + { + Lamp1RemainingPercent = new IntFeedback("Lamp1RemainingPercent", () => _Lamp1RemainingPercent); + Lamp2RemainingPercent = new IntFeedback("Lamp2RemainingPercent", () => _Lamp2RemainingPercent); + + InputMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "computer1", "\x02\x03\x00\x00\x02\x01\x01\x09" }, + { "computer2", "\x02\x03\x00\x00\x02\x01\x02\x0a" }, + { "computer3", "\x02\x03\x00\x00\x02\x01\x03\x0b" }, + { "hdmi", "\x02\x03\x00\x00\x02\x01\x1a\x22" }, + { "dp", "\x02\x03\x00\x00\x02\x01\x1b\x23" }, + { "video", "\x02\x03\x00\x00\x02\x01\x06\x0e" }, + { "viewer", "\x02\x03\x00\x00\x02\x01\x1f\x27" }, + { "network", "\x02\x03\x00\x00\x02\x01\x20\x28" }, + }; + } + + void IsConnected_OutputChange(object sender, EventArgs e) + { + + } + + public void SetEnable(bool state) + { + var tcp = CommunicationMethod as GenericTcpIpClient; + if (tcp != null) + { + tcp.Connect(); + } + } + + public override void PowerOn() + { + SendText("\x02\x00\x00\x00\x00\x02"); + } + + public override void PowerOff() + { + SendText("\x02\x01\x00\x00\x00\x03"); + } + + public void PictureMuteOn() + { + SendText("\x02\x10\x00\x00\x00\x12"); + } + + public void PictureMuteOff() + { + SendText("\x02\x11\x00\x00\x00\x13"); + } + + public void GetRunningStatus() + { + SendText("\x00\x85\x00\x00\x01\x01\x87"); + } + + public void GetLampRemaining(int lampNum) + { + if (!_PowerIsOn) return; + + var bytes = new byte[]{0x03,0x96,0x00,0x00,0x02,0x00,0x04}; + if (lampNum == 2) + bytes[5] = 0x01; + SendBytes(AppendChecksum(bytes)); + } + + public void SelectInput(string inputKey) + { + if (InputMap.ContainsKey(inputKey)) + SendText(InputMap[inputKey]); + } + + void SendText(string text) + { + if (CommunicationMethod != null) + CommunicationMethod.SendText(text); + } + + void SendBytes(byte[] bytes) + { + if (CommunicationMethod != null) + CommunicationMethod.SendBytes(bytes); + } + + byte[] AppendChecksum(byte[] bytes) + { + byte sum = unchecked((byte)bytes.Sum(x => (int)x)); + var retVal = new byte[bytes.Length + 1]; + bytes.CopyTo(retVal, 0); + retVal[retVal.Length - 1] = sum; + return retVal; + } + + protected override void CommunicationMethod_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs args) + { + var bytes = args.Bytes; + ParseBytes(args.Bytes); + } + + void ParseBytes(byte[] bytes) + { + if (bytes[0] == 0x22) + { + // Power on + if (bytes[1] == 0x00) + { + _PowerIsOn = true; + PowerIsOnFeedback.FireUpdate(); + } + // Power off + else if (bytes[1] == 0x01) + { + _PowerIsOn = false; + PowerIsOnFeedback.FireUpdate(); + } + } + // Running Status + else if (bytes[0] == 0x20 && bytes[1] == 0x85 && bytes[4] == 0x10) + { + var operationStates = new Dictionary + { + { 0x00, "Standby" }, + { 0x04, "Power On" }, + { 0x05, "Cooling" }, + { 0x06, "Standby (error)" }, + { 0x0f, "Standby (power saving" }, + { 0x10, "Network Standby" }, + { 0xff, "Not supported" } + }; + + var newPowerIsOn = bytes[7] == 0x01; + if (newPowerIsOn != _PowerIsOn) + { + _PowerIsOn = newPowerIsOn; + PowerIsOnFeedback.FireUpdate(); + } + + Debug.Console(2, this, "PowerIsOn={0}\rCooling={1}\rPowering on/off={2}\rStatus={3}", + _PowerIsOn, + bytes[8] == 0x01, + bytes[9] == 0x01, + operationStates[bytes[10]]); + + } + // Lamp remaining + else if (bytes[0] == 0x23 && bytes[1] == 0x96 && bytes[4] == 0x06 && bytes[6] == 0x04) + { + var newValue = bytes[7]; + if (bytes[5] == 0x00) + { + if (newValue != _Lamp1RemainingPercent) + { + _Lamp1RemainingPercent = newValue; + Lamp1RemainingPercent.FireUpdate(); + } + } + else + { + if (newValue != _Lamp2RemainingPercent) + { + _Lamp2RemainingPercent = newValue; + Lamp2RemainingPercent.FireUpdate(); + } + } + Debug.Console(0, this, "Lamp {0}, {1}% remaining", (bytes[5] + 1), bytes[7]); + } + + } + + + #region INonStandardControls Members + + public Dictionary> GetNonStandardControls() + { + return new Dictionary> + { + { CommonBoolCue.PowerOn, o => PowerOn() }, + { CommonBoolCue.PowerOff, o => PowerOff() }, + { Cue.BoolCue("PictureMute", 0), o => + { + if((bool)o) + PictureMuteOn(); + else + PictureMuteOff(); } }, + { Cue.UShortCue("GetLampRemaining", 0), o => GetLampRemaining((int) o) }, + { Cue.StringCue("SelectInput", 0), o => SelectInput((String)o) } + }; + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/PanasonicThDisplay.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/PanasonicThDisplay.cs new file mode 100644 index 00000000..01d3713d --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/PanasonicThDisplay.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Displays +{ + /// + /// + /// + public class PanasonicThefDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, ICommunicationMonitor + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + + #region Command constants + public const string InputGetCmd = "\x02QMI\x03"; + public const string Hdmi1Cmd = "\x02IMS:HM1\x03"; + public const string Hdmi2Cmd = "\x02IMS:HM2\x03"; + public const string Hdmi3Cmd = ""; + public const string Hdmi4Cmd = ""; + public const string Dp1Cmd = ""; + public const string Dp2Cmd = ""; + public const string Dvi1Cmd = "\x02IMS:DV1"; + public const string Video1Cmd = ""; + public const string VgaCmd = ""; + public const string RgbCmd = ""; + + public const string PowerOnCmd = "\x02PON\x03"; + public const string PowerOffCmd = "\x02POF\x03"; + public const string PowerToggleIrCmd = ""; + + public const string MuteOffCmd = "\x02AMT:0\x03"; + public const string MuteOnCmd = "\x02AMT:1\x03"; + public const string MuteToggleIrCmd = "\x02AMT\x03"; + public const string MuteGetCmd = "\x02QAM\x03"; + + public const string VolumeGetCmd = "\x02QAV\x03"; + public const string VolumeLevelPartialCmd = "\x02AVL:"; // + public const string VolumeUpCmd = "\x02AUU\x03"; + public const string VolumeDownCmd = "\x02AUD\x03"; + + public const string MenuIrCmd = ""; + public const string UpIrCmd = ""; + public const string DownIrCmd = ""; + public const string LeftIrCmd = ""; + public const string RightIrCmd = ""; + public const string SelectIrCmd = ""; + public const string ExitIrCmd = ""; + #endregion + + bool _PowerIsOn; + bool _IsWarmingUp; + bool _IsCoolingDown; + ushort _VolumeLevel; + bool _IsMuted; + + protected override Func PowerIsOnFeedbackFunc { get { return () => _PowerIsOn; } } + protected override Func IsCoolingDownFeedbackFunc { get { return () => _IsCoolingDown; } } + protected override Func IsWarmingUpFeedbackFunc { get { return () => _IsWarmingUp; } } + protected override Func CurrentInputFeedbackFunc { get { return () => "Not Implemented"; } } + + + /// + /// Constructor for IBasicCommunication + /// + public PanasonicThefDisplay(string key, string name, IBasicCommunication comm) + : base(key, name) + { + Communication = comm; + Init(); + } + /// + /// Constructor for TCP + /// + public PanasonicThefDisplay(string key, string name, string hostname, int port) + : base(key, name) + { + Communication = new GenericTcpIpClient(key + "-tcp", hostname, port, 5000); + Init(); + } + + + /// + /// Constructor for COM + /// + public PanasonicThefDisplay(string key, string name, ComPort port, ComPort.ComPortSpec spec) + : base(key, name) + { + Communication = new ComPortController(key + "-com", port, spec); + Init(); + } + + void Init() + { + PortGather = new CommunicationGather(Communication, '\x0d'); + PortGather.LineReceived += this.Port_LineReceived; + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 30000, 120000, 300000, "\x02QPW\x03"); // Query Power + + InputPorts.Add(new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi1), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi2), this)); + + InputPorts.Add(new RoutingInputPort(RoutingPortNames.DviIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Dvi, new Action(InputDvi1), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.CompositeIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Composite, new Action(InputVideo1), this)); + InputPorts.Add(new RoutingInputPort(RoutingPortNames.VgaIn, eRoutingSignalType.Video, + eRoutingPortConnectionType.Vga, new Action(InputVga), this)); + + + VolumeLevelFeedback = new IntFeedback(() => { return _VolumeLevel; }); + MuteFeedback = new BoolFeedback(() => _IsMuted); + + // new BoolCueActionPair(CommonBoolCue.Menu, b => { if(b) Send(MenuIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Up, b => { if(b) Send(UpIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Down, b => { if(b) Send(DownIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Left, b => { if(b) Send(LeftIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Right, b => { if(b) Send(RightIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Select, b => { if(b) Send(SelectIrCmd); }), + // new BoolCueActionPair(CommonBoolCue.Exit, b => { if(b) Send(ExitIrCmd); }), + //}; + } + + ~PanasonicThefDisplay() + { + PortGather = null; + } + + public override bool CustomActivate() + { + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + return true; + } + + public override FeedbackCollection Feedbacks + { + get + { + var list = base.Feedbacks; + list.AddRange(new List + { + + }); + return list; + } + } + + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + if (Debug.Level == 2) + Debug.Console(2, this, "Received: '{0}'", ComTextHelper.GetEscapedText(args.Text)); + + if (args.Text=="DO SOMETHING HERE EVENTUALLY") + { + _IsMuted = true; + MuteFeedback.FireUpdate(); + } + } + + void Send(string s) + { + if (Debug.Level == 2) + Debug.Console(2, this, "Send: '{0}'", ComTextHelper.GetEscapedText(s)); + Communication.SendText(s); + } + + + public override void PowerOn() + { + Send(PowerOnCmd); + if (!PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) + { + _IsWarmingUp = true; + IsWarmingUpFeedback.FireUpdate(); + // Fake power-up cycle + WarmupTimer = new CTimer(o => + { + _IsWarmingUp = false; + _PowerIsOn = true; + IsWarmingUpFeedback.FireUpdate(); + PowerIsOnFeedback.FireUpdate(); + }, WarmupTime); + } + } + + public override void PowerOff() + { + // If a display has unreliable-power off feedback, just override this and + // remove this check. + if (PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) + { + Send(PowerOffCmd); + _IsCoolingDown = true; + _PowerIsOn = false; + PowerIsOnFeedback.FireUpdate(); + IsCoolingDownFeedback.FireUpdate(); + // Fake cool-down cycle + CooldownTimer = new CTimer(o => + { + Debug.Console(2, this, "Cooldown timer ending"); + _IsCoolingDown = false; + IsCoolingDownFeedback.FireUpdate(); + }, CooldownTime); + } + } + + public override void PowerToggle() + { + if (PowerIsOnFeedback.BoolValue && !IsWarmingUpFeedback.BoolValue) + PowerOff(); + else if (!PowerIsOnFeedback.BoolValue && !IsCoolingDownFeedback.BoolValue) + PowerOn(); + } + + public void InputHdmi1() + { + Send(Hdmi1Cmd); + } + + public void InputHdmi2() + { + Send(Hdmi2Cmd); + } + + public void InputHdmi3() + { + Send(Hdmi3Cmd); + } + + public void InputHdmi4() + { + Send(Hdmi4Cmd); + } + + public void InputDisplayPort1() + { + Send(Dp1Cmd); + } + + public void InputDisplayPort2() + { + Send(Dp2Cmd); + } + + public void InputDvi1() + { + Send(Dvi1Cmd); + } + + public void InputVideo1() + { + Send(Video1Cmd); + } + + public void InputVga() + { + Send(VgaCmd); + } + + public void InputRgb() + { + Send(RgbCmd); + } + + public override void ExecuteSwitch(object selector) + { + if (selector is Action) + (selector as Action).Invoke(); + else + Debug.Console(1, this, "WARNING: ExecuteSwitch cannot handle type {0}", selector.GetType()); + //Send((string)selector); + } + + void SetVolume(ushort level) + { + var levelString = string.Format("{0}{1:X3}\x03", VolumeLevelPartialCmd, level); + + //Debug.Console(2, this, "Volume:{0}", ComTextHelper.GetEscapedText(levelString)); + _VolumeLevel = level; + VolumeLevelFeedback.FireUpdate(); + } + + #region IBasicVolumeWithFeedback Members + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + public void MuteOff() + { + Send(MuteOffCmd); + } + + public void MuteOn() + { + Send(MuteOnCmd); + } + + void IBasicVolumeWithFeedback.SetVolume(ushort level) + { + SetVolume(level); + } + + #endregion + + #region IBasicVolumeControls Members + + public void MuteToggle() + { + Send(MuteToggleIrCmd); + } + + public void VolumeDown(bool pressRelease) + { + //throw new NotImplementedException(); +//#warning need incrementer for these + SetVolume(_VolumeLevel++); + } + + public void VolumeUp(bool pressRelease) + { + //throw new NotImplementedException(); + SetVolume(_VolumeLevel--); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/SamsungMDCDisplay.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/SamsungMDCDisplay.cs new file mode 100644 index 00000000..c7d8d0d5 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Display/SamsungMDCDisplay.cs @@ -0,0 +1,620 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.CrestronThread; +using Crestron.SimplSharpPro; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Displays +{ + /// + /// + /// + public class SamsungMDC : TwoWayDisplayBase, IBasicVolumeWithFeedback, ICommunicationMonitor, IInputDisplayPort1, IInputDisplayPort2, + IInputHdmi1, IInputHdmi2, IInputHdmi3, IInputHdmi4 + { + public IBasicCommunication Communication { get; private set; } + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + public byte ID { get; private set; } + + bool LastCommandSentWasVolume; + + bool _PowerIsOn; + bool _IsWarmingUp; + bool _IsCoolingDown; + ushort _VolumeLevelForSig; + int _LastVolumeSent; + bool _IsMuted; + RoutingInputPort _CurrentInputPort; + byte[] IncomingBuffer = new byte[]{}; + ActionIncrementer VolumeIncrementer; + bool VolumeIsRamping; + public bool IsInStandby { get; private set; } + bool IsPoweringOnIgnorePowerFb; + + protected override Func PowerIsOnFeedbackFunc { get { return () => _PowerIsOn; } } + protected override Func IsCoolingDownFeedbackFunc { get { return () => _IsCoolingDown; } } + protected override Func IsWarmingUpFeedbackFunc { get { return () => _IsWarmingUp; } } + protected override Func CurrentInputFeedbackFunc { get { return () => _CurrentInputPort.Key; } } + + /// + /// Constructor for IBasicCommunication + /// + public SamsungMDC(string key, string name, IBasicCommunication comm, string id) + : base(key, name) + { + Communication = comm; + Communication.BytesReceived += new EventHandler(Communication_BytesReceived); + + ID = id == null ? (byte)0x01 : Convert.ToByte(id, 16); // If id is null, set default value of 0x01, otherwise assign value passed in constructor + Init(); + } + + /// + /// Constructor for TCP + /// + public SamsungMDC(string key, string name, string hostname, int port, string id) + : base(key, name) + { + Communication = new GenericTcpIpClient(key + "-tcp", hostname, port, 5000); + ID = id == null ? (byte)0x01 : Convert.ToByte(id, 16); // If id is null, set default value of 0x01, otherwise assign value passed in constructor + Init(); + } + + /// + /// Constructor for COM + /// + public SamsungMDC(string key, string name, ComPort port, ComPort.ComPortSpec spec, string id) + : base(key, name) + { + Communication = new ComPortController(key + "-com", port, spec); + //Communication.TextReceived += new EventHandler(Communication_TextReceived); + + ID = id == null ? (byte)0x01 : Convert.ToByte(id, 16); // If id is null, set default value of 0x01, otherwise assign value passed in constructor + Init(); + } + + void AddRoutingInputPort(RoutingInputPort port, byte fbMatch) + { + port.FeedbackMatchObject = fbMatch; + InputPorts.Add(port); + } + + void Init() + { + WarmupTime = 10000; + CooldownTime = 8000; + + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 2000, 120000, 300000, StatusGet); + DeviceManager.AddDevice(CommunicationMonitor); + + VolumeIncrementer = new ActionIncrementer(655, 0, 65535, 800, 80, + v => SetVolume((ushort)v), + () => _LastVolumeSent); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi1), this), 0x21); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn1PC, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi1PC), this), 0x22); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi2), this), 0x23); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn2PC, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi2PC), this), 0x24); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi3), this), 0x32); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.DisplayPortIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DisplayPort, new Action(InputDisplayPort1), this), 0x25); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.DviIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Dvi, new Action(InputDvi1), this), 0x18); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.CompositeIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Composite, new Action(InputVideo1), this), 0x08); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.RgbIn1, eRoutingSignalType.Video, + eRoutingPortConnectionType.Vga, new Action(InputRgb1), this), 0x14); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.RgbIn2, eRoutingSignalType.Video, + eRoutingPortConnectionType.Rgb, new Action(new Action(InputRgb2)), this), 0x1E); + + VolumeLevelFeedback = new IntFeedback(() => { return _VolumeLevelForSig; }); + MuteFeedback = new BoolFeedback(() => _IsMuted); + + StatusGet(); + } + + /// + /// + /// + /// + public override bool CustomActivate() + { + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + return true; + } + + public override FeedbackCollection Feedbacks + { + get + { + var list = base.Feedbacks; + list.AddRange(new List + { + VolumeLevelFeedback, + MuteFeedback, + CurrentInputFeedback + }); + return list; + } + } + + /// + /// / + /// + /// + void Communication_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e) + { + // This is probably not thread-safe buffering + // Append the incoming bytes with whatever is in the buffer + var newBytes = new byte[IncomingBuffer.Length + e.Bytes.Length]; + IncomingBuffer.CopyTo(newBytes, 0); + e.Bytes.CopyTo(newBytes, IncomingBuffer.Length); + + if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 + Debug.Console(2, this, "Received:{0}", ComTextHelper.GetEscapedText(newBytes)); + + // Need to find AA FF and have + for (int i = 0; i < newBytes.Length; i++) + { + if (newBytes[i] == 0xAA && newBytes[i + 1] == 0xFF) + { + newBytes = newBytes.Skip(i).ToArray(); // Trim off junk if there's "dirt" in the buffer + + // parse it + // If it's at least got the header, then process it, + while (newBytes.Length > 4 && newBytes[0] == 0xAA && newBytes[1] == 0xFF) + { + var msgLen = newBytes[3]; + // if the buffer is shorter than the header (3) + message (msgLen) + checksum (1), + // give and save it for next time + if (newBytes.Length < msgLen + 4) + break; + + // Good length, grab the message + var message = newBytes.Skip(4).Take(msgLen).ToArray(); + + // At this point, the ack/nak is the first byte + if (message[0] == 0x41) + { + switch (message[1]) // type byte + { + case 0x00: // General status + //UpdatePowerFB(message[2], message[5]); // "power" can be misrepresented when the display sleeps + + // Handle the first power on fb when waiting for it. + if (IsPoweringOnIgnorePowerFb && message[2] == 0x01) + IsPoweringOnIgnorePowerFb = false; + // Ignore general-status power off messages when powering up + if (!(IsPoweringOnIgnorePowerFb && message[2] == 0x00)) + UpdatePowerFB(message[2]); + UpdateVolumeFB(message[3]); + UpdateMuteFb(message[4]); + UpdateInputFb(message[5]); + break; + + case 0x11: + UpdatePowerFB(message[2]); + break; + + case 0x12: + UpdateVolumeFB(message[2]); + break; + + case 0x13: + UpdateMuteFb(message[2]); + break; + + case 0x14: + UpdateInputFb(message[2]); + break; + + default: + break; + } + } + // Skip over what we've used and save the rest for next time + newBytes = newBytes.Skip(5 + msgLen).ToArray(); + } + + break; // parsing will mean we can stop looking for header in loop + } + } + + // Save whatever partial message is here + IncomingBuffer = newBytes; + } + + /// + /// + /// + void UpdatePowerFB(byte powerByte) + { + var newVal = powerByte == 1; + if (newVal != _PowerIsOn) + { + _PowerIsOn = newVal; + PowerIsOnFeedback.FireUpdate(); + } + } + + /// + /// Updates power status from general updates where source is included. + /// Compensates for errant standby / power off hiccups by ignoring + /// power off states with input < 0x10 + /// + void UpdatePowerFB(byte powerByte, byte inputByte) + { + // This should reject errant power feedbacks when switching away from input on standby. + if (powerByte == 0x01 && inputByte < 0x10) + IsInStandby = true; + if (powerByte == 0x00 && IsInStandby) // Ignore power off if coming from standby - glitch + { + IsInStandby = false; + return; + } + + UpdatePowerFB(powerByte); + } + + /// + /// + /// + void UpdateVolumeFB(byte b) + { + var newVol = (ushort)NumericalHelpers.Scale((double)b, 0, 100, 0, 65535); + if (!VolumeIsRamping) + _LastVolumeSent = newVol; + if (newVol != _VolumeLevelForSig) + { + _VolumeLevelForSig = newVol; + VolumeLevelFeedback.FireUpdate(); + } + } + + /// + /// + /// + void UpdateMuteFb(byte b) + { + var newMute = b == 1; + if (newMute != _IsMuted) + { + _IsMuted = newMute; + MuteFeedback.FireUpdate(); + } + } + + /// + /// + /// + void UpdateInputFb(byte b) + { + var newInput = InputPorts.FirstOrDefault(i => i.FeedbackMatchObject.Equals(b)); + if (newInput != null && newInput != _CurrentInputPort) + { + _CurrentInputPort = newInput; + CurrentInputFeedback.FireUpdate(); + } + } + + /// + /// Formats an outgoing message. Replaces third byte with ID and replaces last byte with checksum + /// + /// + void SendBytes(byte[] b) + { + if (LastCommandSentWasVolume) // If the last command sent was volume + if (b[1] != 0x12) // Check if this command is volume, and if not, delay this command + CrestronEnvironment.Sleep(100); + + b[2] = ID; + // append checksum by adding all bytes, except last which should be 00 + int checksum = 0; + for (var i = 1; i < b.Length - 1; i++) // add 2nd through 2nd-to-last bytes + { + checksum += b[i]; + } + checksum = checksum & 0x000000FF; // mask off MSBs + b[b.Length - 1] = (byte)checksum; + if(Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 + Debug.Console(2, this, "Sending:{0}", ComTextHelper.GetEscapedText(b)); + + if (b[1] == 0x12) + LastCommandSentWasVolume = true; + else + LastCommandSentWasVolume = false; + + Communication.SendBytes(b); + } + + + /// + /// + /// + public void StatusGet() + { + SendBytes(new byte[] { 0xAA, 0x00, 0x00, 0x00, 0x00 }); + } + + /// + /// + /// + public override void PowerOn() + { + IsPoweringOnIgnorePowerFb = true; + //Send(PowerOnCmd); + SendBytes(new byte[] { 0xAA, 0x11, 0x00, 0x01, 0x01, 0x00 }); + if (!PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) + { + _IsWarmingUp = true; + IsWarmingUpFeedback.FireUpdate(); + // Fake power-up cycle + WarmupTimer = new CTimer(o => + { + _IsWarmingUp = false; + _PowerIsOn = true; + IsWarmingUpFeedback.FireUpdate(); + PowerIsOnFeedback.FireUpdate(); + }, WarmupTime); + } + } + + /// + /// + /// + public override void PowerOff() + { + IsPoweringOnIgnorePowerFb = false; + // If a display has unreliable-power off feedback, just override this and + // remove this check. + if (!_IsWarmingUp && !_IsCoolingDown) // PowerIsOnFeedback.BoolValue && + { + //Send(PowerOffCmd); + SendBytes(new byte[] { 0xAA, 0x11, 0x00, 0x01, 0x00, 0x00 }); + _IsCoolingDown = true; + _PowerIsOn = false; + PowerIsOnFeedback.FireUpdate(); + IsCoolingDownFeedback.FireUpdate(); + // Fake cool-down cycle + CooldownTimer = new CTimer(o => + { + _IsCoolingDown = false; + IsCoolingDownFeedback.FireUpdate(); + }, CooldownTime); + } + } + + public override void PowerToggle() + { + if (PowerIsOnFeedback.BoolValue && !IsWarmingUpFeedback.BoolValue) + PowerOff(); + else if (!PowerIsOnFeedback.BoolValue && !IsCoolingDownFeedback.BoolValue) + PowerOn(); + } + + public void PowerGet() + { + SendBytes(new byte[] { 0xAA, 0x11, 0x00, 0x00, 0x00 }); + } + + public void InputHdmi1() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x21, 0x00 }); + } + + public void InputHdmi1PC() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x22, 0x00 }); + } + + public void InputHdmi2() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x23, 0x00 }); + } + + public void InputHdmi2PC() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x24, 0x00 }); + } + + public void InputHdmi3() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x32, 0x00 }); + } + + public void InputHdmi4() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x34, 0x00 }); + } + + public void InputDisplayPort1() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x25, 0x00 }); + } + + public void InputDisplayPort2() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x26, 0x00 }); + } + + public void InputDvi1() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x18, 0x00 }); + } + + public void InputVideo1() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x08, 0x00 }); + } + + public void InputRgb1() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x14, 0x00 }); + } + + public void InputRgb2() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x01, 0x1E, 0x00 }); + } + + public void InputGet() + { + SendBytes(new byte[] { 0xAA, 0x14, 0x00, 0x00, 0x00 }); + } + + + /// + /// Executes a switch, turning on display if necessary. + /// + /// + public override void ExecuteSwitch(object selector) + { + //if (!(selector is Action)) + // Debug.Console(1, this, "WARNING: ExecuteSwitch cannot handle type {0}", selector.GetType()); + + if (_PowerIsOn) + (selector as Action)(); + else // if power is off, wait until we get on FB to send it. + { + // One-time event handler to wait for power on before executing switch + EventHandler handler = null; // necessary to allow reference inside lambda to handler + handler = (o, a) => + { + if (!_IsWarmingUp) // Done warming + { + IsWarmingUpFeedback.OutputChange -= handler; + (selector as Action)(); + } + }; + IsWarmingUpFeedback.OutputChange += handler; // attach and wait for on FB + PowerOn(); + } + } + + /// + /// Scales the level to the range of the display and sends the command + /// + /// + public void SetVolume(ushort level) + { + _LastVolumeSent = level; + var scaled = (int)NumericalHelpers.Scale(level, 0, 65535, 0, 100); + // The inputs to Scale ensure that byte won't overflow + SendBytes(new byte[] { 0xAA, 0x12, 0x00, 0x01, Convert.ToByte(scaled), 0x00 }); + } + + #region IBasicVolumeWithFeedback Members + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + /// + /// + /// + public void MuteOff() + { + SendBytes(new byte[] { 0xAA, 0x13, 0x00, 0x01, 0x00, 0x00 }); + } + + /// + /// + /// + public void MuteOn() + { + SendBytes(new byte[] { 0xAA, 0x13, 0x00, 0x01, 0x01, 0x00 }); + } + + /// + /// + /// + public void MuteGet() + { + SendBytes(new byte[] { 0xAA, 0x13, 0x00, 0x00, 0x00 }); + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// + /// + public void MuteToggle() + { + if (_IsMuted) + MuteOff(); + else + MuteOn(); + } + + /// + /// + /// + /// + public void VolumeDown(bool pressRelease) + { + if (pressRelease) + { + VolumeIncrementer.StartDown(); + VolumeIsRamping = true; + } + else + { + VolumeIsRamping = false; + VolumeIncrementer.Stop(); + } + } + + /// + /// + /// + /// + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + { + VolumeIncrementer.StartUp(); + VolumeIsRamping = true; + } + else + { + VolumeIsRamping = false; + VolumeIncrementer.Stop(); + } + } + + /// + /// + /// + public void VolumeGet() + { + SendBytes(new byte[] { 0xAA, 0x12, 0x00, 0x00, 0x00 }); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Crestron Lighting/Din8sw8.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Crestron Lighting/Din8sw8.cs new file mode 100644 index 00000000..e371d2d1 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Crestron Lighting/Din8sw8.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.Lighting; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.CrestronIO; + +namespace PepperDash.Essentials.Devices.Common.Environment.Lighting +{ + public class Din8sw8Controller : Device, ISwitchedOutputCollection + { + // Need to figure out some sort of interface to make these switched outputs behave like processor relays so they can be used interchangably + + public Din8Sw8 SwitchModule { get; private set; } + + /// + /// Collection of generic switched outputs + /// + public Dictionary SwitchedOutputs { get; private set; } + + public Din8sw8Controller(string key, uint cresnetId) + : base(key) + { + SwitchedOutputs = new Dictionary(); + + SwitchModule = new Din8Sw8(cresnetId, Global.ControlSystem); + + if (SwitchModule.Register() != eDeviceRegistrationUnRegistrationResponse.Success) + { + Debug.Console(2, this, "Error registering Din8sw8. Reason: {0}", SwitchModule.RegistrationFailureReason); + } + + PopulateDictionary(); + } + + public override bool CustomActivate() + { + + + return base.CustomActivate(); + } + + /// + /// Populates the generic collection with the loads from the Crestron collection + /// + void PopulateDictionary() + { + foreach (var item in SwitchModule.SwitchedLoads) + { + SwitchedOutputs.Add(item.Number, new Din8sw8Output(item)); + } + } + } + + /// + /// Wrapper class to + /// + public class Din8sw8Output : ISwitchedOutput + { + SwitchedLoadWithOverrideParameter SwitchedOutput; + + public BoolFeedback OutputIsOnFeedback { get; protected set; } + + public Din8sw8Output(SwitchedLoadWithOverrideParameter switchedOutput) + { + SwitchedOutput = switchedOutput; + + OutputIsOnFeedback = new BoolFeedback(new Func(() => SwitchedOutput.IsOn)); + } + + public void On() + { + SwitchedOutput.FullOn(); + } + + public void Off() + { + SwitchedOutput.FullOff(); + } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Lutron/LutronQuantum.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Lutron/LutronQuantum.cs new file mode 100644 index 00000000..adcceb4f --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Lutron/LutronQuantum.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Lighting; + +namespace PepperDash.Essentials.Devices.Common.Environment.Lutron +{ + public class LutronQuantumArea : LightingBase, ILightingMasterRaiseLower, ICommunicationMonitor + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + + CTimer SubscribeAfterLogin; + + public string IntegrationId; + string Username; + string Password; + + const string Delimiter = "\x0d\x0a"; + const string Set = "#"; + const string Get = "?"; + + public LutronQuantumArea(string key, string name, IBasicCommunication comm, LutronQuantumPropertiesConfig props) + : base(key, name) + { + Communication = comm; + + IntegrationId = props.IntegrationId; + + if (props.Control.Method != eControlMethod.Com) + { + + Username = props.Control.TcpSshProperties.Username; + Password = props.Control.TcpSshProperties.Password; + } + + + LightingScenes = props.Scenes; + + var socket = comm as ISocketStatus; + if (socket != null) + { + // IP Control + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + else + { + // RS-232 Control + } + + Communication.TextReceived += new EventHandler(Communication_TextReceived); + + PortGather = new CommunicationGather(Communication, Delimiter); + PortGather.LineReceived += new EventHandler(PortGather_LineReceived); + + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 120000, 120000, 300000, "?ETHERNET,0\x0d\x0a"); + } + } + + public override bool CustomActivate() + { + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + return true; + } + + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.Console(2, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); + + if (e.Client.IsConnected) + { + // Tasks on connect + } + } + + /// + /// Checks for responses that do not contain the delimiter + /// + /// + /// + void Communication_TextReceived(object sender, GenericCommMethodReceiveTextArgs args) + { + Debug.Console(2, this, "Text Received: '{0}'", args.Text); + + if (args.Text.Contains("login:")) + { + // Login + SendLine(Username); + } + else if (args.Text.Contains("password:")) + { + // Login + SendLine(Password); + SubscribeAfterLogin = new CTimer(x => SubscribeToFeedback(), null, 5000); + + } + else if (args.Text.Contains("Access Granted")) + { + if (SubscribeAfterLogin != null) + { + SubscribeAfterLogin.Stop(); + } + SubscribeToFeedback(); + } + } + + /// + /// Handles all responses that contain the delimiter + /// + /// + /// + void PortGather_LineReceived(object sender, GenericCommMethodReceiveTextArgs args) + { + Debug.Console(2, this, "Line Received: '{0}'", args.Text); + + try + { + if (args.Text.Contains("~AREA")) + { + var response = args.Text.Split(','); + + var integrationId = response[1]; + + if (integrationId != IntegrationId) + { + Debug.Console(2, this, "Response is not for correct Integration ID"); + return; + } + else + { + var action = Int32.Parse(response[2]); + + switch (action) + { + case (int)eAction.Scene: + { + var scene = response[3]; + CurrentLightingScene = LightingScenes.FirstOrDefault(s => s.ID.Equals(scene)); + + OnLightingSceneChange(); + + break; + } + default: + break; + } + } + } + } + catch (Exception e) + { + Debug.Console(2, this, "Error parsing response:\n{0}", e); + } + } + + /// + /// Subscribes to feedback + /// + public void SubscribeToFeedback() + { + Debug.Console(1, "Sending Monitoring Subscriptions"); + SendLine("#MONITORING,6,1"); + SendLine("#MONITORING,8,1"); + SendLine("#MONITORING,5,2"); + } + + /// + /// Recalls the specified scene + /// + /// + /// + + public override void SelectScene(LightingScene scene) + { + Debug.Console(1, this, "Selecting Scene: '{0}'", scene.Name); + SendLine(string.Format("{0}AREA,{1},{2},{3}", Set, IntegrationId, (int)eAction.Scene, scene.ID)); + } + + /// + /// Begins raising the lights in the area + /// + public void MasterRaise() + { + SendLine(string.Format("{0}AREA,{1},{2}", Set, IntegrationId, (int)eAction.Raise)); + } + + /// + /// Begins lowering the lights in the area + /// + public void MasterLower() + { + SendLine(string.Format("{0}AREA,{1},{2}", Set, IntegrationId, (int)eAction.Lower)); + } + + /// + /// Stops the current raise/lower action + /// + public void MasterRaiseLowerStop() + { + SendLine(string.Format("{0}AREA,{1},{2}", Set, IntegrationId, (int)eAction.Stop)); + } + + /// + /// Appends the delimiter and sends the string + /// + /// + public void SendLine(string s) + { + Debug.Console(2, this, "TX: '{0}'", s); + Communication.SendText(s + Delimiter); + } + } + + public enum eAction : int + { + SetLevel = 1, + Raise = 2, + Lower = 3, + Stop = 4, + Scene = 6, + DaylightMode = 7, + OccupancyState = 8, + OccupancyMode = 9, + OccupiedLevelOrScene = 12, + UnoccupiedLevelOrScene = 13, + HyperionShaddowSensorOverrideState = 26, + HyperionBrightnessSensorOverrideStatue = 27 + } + + public class LutronQuantumPropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + public ControlPropertiesConfig Control { get; set; } + + public string IntegrationId { get; set; } + public List Scenes { get; set; } + + // Moved to use existing properties in Control object + // public string Username { get; set; } + // public string Password { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Somfy/RelayControlledShade.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Somfy/RelayControlledShade.cs new file mode 100644 index 00000000..22e06d03 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Environment/Somfy/RelayControlledShade.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.CrestronIO; +using PepperDash.Essentials.Core.Shades; + +namespace PepperDash.Essentials.Devices.Common.Environment.Somfy +{ + /// + /// Controls a single shade using three relays + /// + public class RelayControlledShade : ShadeBase, IShadesOpenCloseStop + { + RelayControlledShadeConfigProperties Config; + + ISwitchedOutput OpenRelay; + ISwitchedOutput StopOrPresetRelay; + ISwitchedOutput CloseRelay; + + int RelayPulseTime; + + public string StopOrPresetButtonLabel { get; set; } + + public RelayControlledShade(string key, string name, RelayControlledShadeConfigProperties config) + : base(key, name) + { + Config = config; + + RelayPulseTime = Config.RelayPulseTime; + + StopOrPresetButtonLabel = Config.StopOrPresetLabel; + + } + + public override bool CustomActivate() + { + //Create ISwitchedOutput objects based on props + OpenRelay = GetSwitchedOutputFromDevice(Config.Relays.Open); + StopOrPresetRelay = GetSwitchedOutputFromDevice(Config.Relays.StopOrPreset); + CloseRelay = GetSwitchedOutputFromDevice(Config.Relays.Close); + + + return base.CustomActivate(); + } + + public override void Open() + { + Debug.Console(1, this, "Opening Shade: '{0}'", this.Name); + + PulseOutput(OpenRelay, RelayPulseTime); + } + + public override void StopOrPreset() + { + Debug.Console(1, this, "Stopping or recalling preset on Shade: '{0}'", this.Name); + + PulseOutput(StopOrPresetRelay, RelayPulseTime); + } + + public override void Close() + { + Debug.Console(1, this, "Closing Shade: '{0}'", this.Name); + + PulseOutput(CloseRelay, RelayPulseTime); + } + + void PulseOutput(ISwitchedOutput output, int pulseTime) + { + output.On(); + CTimer pulseTimer = new CTimer(new CTimerCallbackFunction((o) => output.Off()), pulseTime); + } + + /// + /// Attempts to get the port on teh specified device from config + /// + /// + /// + ISwitchedOutput GetSwitchedOutputFromDevice(IOPortConfig relayConfig) + { + var portDevice = DeviceManager.GetDeviceForKey(relayConfig.PortDeviceKey); + + if (portDevice != null) + { + return (portDevice as ISwitchedOutputCollection).SwitchedOutputs[relayConfig.PortNumber]; + } + else + { + Debug.Console(1, this, "Error: Unable to get relay on port '{0}' from device with key '{1}'", relayConfig.PortNumber, relayConfig.PortDeviceKey); + return null; + } + } + + } + + public class RelayControlledShadeConfigProperties + { + public int RelayPulseTime { get; set; } + public ShadeRelaysConfig Relays { get; set; } + public string StopOrPresetLabel { get; set; } + + public class ShadeRelaysConfig + { + public IOPortConfig Open { get; set; } + public IOPortConfig StopOrPreset { get; set; } + public IOPortConfig Close { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Essentials Devices Common.csproj b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Essentials Devices Common.csproj new file mode 100644 index 00000000..cf723ecf --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Essentials Devices Common.csproj @@ -0,0 +1,205 @@ + + + Release + AnyCPU + 9.0.30729 + 2.0 + {892B761C-E479-44CE-BD74-243E9214AF13} + Library + Properties + PepperDash.Essentials.Devices.Common + Essentials Devices Common + {0B4745B0-194B-4BB6-8E21-E9057CA92300};{4D628B5B-2FBC-4AA6-8C16-197242AEB884};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + WindowsCE + E2BECB1F-8C8C-41ba-B736-9BE7D946A398 + 5.0 + SmartDeviceProject1 + v3.5 + Windows CE + + + + + .allowedReferenceRelatedFileExtensions + true + full + false + bin\ + DEBUG;TRACE; + prompt + 4 + 512 + true + true + off + + + .allowedReferenceRelatedFileExtensions + none + true + bin\ + prompt + 4 + 512 + true + true + off + + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.DeviceSupport.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.Gateways.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.GeneralIO.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.Lighting.dll + + + + False + ..\..\references\PepperDash_Core.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpCustomAttributesInterface.dll + False + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpHelperInterface.dll + False + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpNewtonsoft.dll + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpPro.exe + False + + + False + ..\..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpReflectionInterface.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5} + PepperDash_Essentials_Core + + + + + + + + + rem S# Pro preparation will execute after these operations + + \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertsEndpointStatusServer.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertsEndpointStatusServer.cs new file mode 100644 index 00000000..5867b51a --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertsEndpointStatusServer.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System.Text.RegularExpressions; +using Crestron.SimplSharp.Net.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +using Crestron.SimplSharp.CrestronSockets; + + +namespace PepperDash.Essentials.Devices.Common +{ + + /***** + * TODO JTA: Add Polling + * TODO JTA: Move all the registration commnads to the EvertEndpoint class. + * + * + * + * + * + */ + + public class EvertzEndpointStatusServer : Device + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + public bool isSubscribed; + public string Address; + public GenericUdpServer Server; + + + + /// + /// Shows received lines as hex + /// + public bool ShowHexResponse { get; set; } + public Dictionary Endpoints; + public Dictionary ServerIdByEndpointIp; + + public EvertzEndpointStatusServer(string key, string name, GenericUdpServer server, EvertzEndpointStatusServerPropertiesConfig props) : + base(key, name) + { + Server = server; + Address = props.serverHostname; + Server.DataRecievedExtra += new EventHandler(_Server_DataRecievedExtra); + + Server.Connect(); + Endpoints = new Dictionary(); + + //CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + } + + + + //TODO JTA: Move this method and process over to the endpoint itself. return a bool. + public bool RegisterEvertzEndpoint (EvertzEndpoint device) + { + + if (Endpoints.ContainsKey(device.Address) == false) + { + Endpoints.Add(device.Address, device); + } + + return true; + } + + + + + void _Server_DataRecievedExtra(object sender, GenericUdpReceiveTextExtraArgs e) + { + Debug.Console(2, this, "_Server_DataRecievedExtra:\nIP:{0}\nPort:{1}\nText{2}\nBytes:{3} ", e.IpAddress, e.Port, e.Text, e.Bytes); + } + + public override bool CustomActivate() + { + /* + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + */ + + + + + + return true; + } + + } + + + + + + + + + + + + public class EvertzPortRestResponse + { + public string id; + public string name; + public string type; + public string value; + + } + + public class EvertsStatusRequesstResponse + { + public EvertzStatusDataResponse data; + public string error; + } + + public class EvertzStatusDataResponse + { + public List servers; + } + + public class EvertzServerStatusResponse + { + public string id; + public string name; + public EvertsServerStausNotificationsResponse notify; + + } + public class EvertsServerStausNotificationsResponse + { + public string ip; + public List parameters; + public string port; + public string protocol; + } + public class EvertzEndpointStatusServerPropertiesConfig + { + + public ControlPropertiesConfig control { get; set; } + public string serverHostname { get; set; } + + } + + } diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpoint.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpoint.cs new file mode 100644 index 00000000..7ab504d1 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpoint.cs @@ -0,0 +1,337 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System.Text.RegularExpressions; +using Crestron.SimplSharp.Net.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + + +namespace PepperDash.Essentials.Devices.Common +{ + + public class EvertzEndpoint : Device + { + + + + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public GenericCommunicationMonitor CommunicationMonitor { get; private set; } + + private GenericHttpClient Client; + public string userName; + public string password; + public string Address; + private bool OnlineStatus; + public BoolFeedback OnlineFeedback; + public IntFeedback PresetFeedback; + + + public bool isSubscribed; + + + + CrestronQueue CommandQueue; + + public Dictionary Ports; + + private string _ControllerKey; + + + private EvertzEndpointStatusServer StatusServer; + private String StatusServerId; + + /// + /// Shows received lines as hex + /// + public bool ShowHexResponse { get; set; } + + public EvertzEndpoint(string key, string name, EvertzEndpointPropertiesConfig props, string type) : + base(key, name) + { + + + this.Address = props.address; + Client = new GenericHttpClient(string.Format("{0}-GenericWebClient", name), string.Format("{0}-GenericWebClient", name), this.Address); + Client.ResponseRecived += new EventHandler(Client_ResponseRecived); + Ports = new Dictionary(); + if (type.ToLower() == "mma10g-trs4k") + { + //create port hdmi 01 + EvertzEndpointPort hdmi1 = new EvertzEndpointPort("HDMI01", "131.0@s", "136.0@s"); + EvertzEndpointPort hdmi2 = new EvertzEndpointPort("HDMI02", "131.1@s", "136.1@s"); + // add to dictionay with all keys + addPortToDictionary(hdmi1); + addPortToDictionary(hdmi2); + } + _ControllerKey = null; + if (props.controllerKey != null) + { + _ControllerKey = props.controllerKey; + } + AddPostActivationAction( () => {PostActivation();}); + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Client, props.CommunicationMonitorProperties); + } + else + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Client, 40000, 120000, 300000, "v.api/apis/EV/SERVERSTATUS"); + } + + + } + + /// + /// Helper method + /// + /// + private void addPortToDictionary(EvertzEndpointPort port) + { + Ports.Add(port.PortName, port); + Ports.Add(port.ResolutionVarID, port); + Ports.Add(port.SyncVarID, port); + //PollForState(port.SyncVarID); + //PollForState(port.ResolutionVarID); + } + + /// + /// + /// + /// + public override bool CustomActivate() + { + + // Create Device -> Constructor fires + // PreActivations get called + // CustomActivate Gets Called Anything that is involved with this single class Ex: Connection, Setup Feedback, Etc. + // After this point all devices are ready for interaction + // PostActivation gets called. Use this for interClass activation. + CommunicationMonitor.Start(); + OnlineFeedback = new BoolFeedback(() => { return OnlineStatus; }); + + + //CrestronConsole.AddNewConsoleCommand(SendLine, "send" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + return true; + } + + /// + /// + /// + private void PostActivation() + { + Debug.Console(2, this, "EvertzEndpoint Post Activation"); + if (_ControllerKey != null) + { + StatusServer = DeviceManager.GetDeviceForKey(_ControllerKey) as EvertzEndpointStatusServer; + StatusServer.RegisterEvertzEndpoint(this); + + // RegisterStatusServer(); + // SendStatusRequest(); + } + // PollAll(); + } + + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) + { + Debug.Console(1, this, "Program stopping. Disabling EvertzStatusServer"); + if (StatusServerId != null) + { + //UnregisterServer(); + } + } + } + + private void ProcessServerStatusRequest(EvertsStatusRequesstResponse status) + { + // var status = JsonConvert.DeserializeObject(SendStatusRequest()); + if (status.error != null) + { + + } + else if (status.data != null) + { + foreach (var server in status.data.servers) + { + if (server.name == string.Format("{0}-{1}", this.Name, StatusServer.Address)) + { + StatusServerId = server.id; + Debug.Console(2, this, "EvertzEndpoint {0} StatusServer {1} Registered ID {2}", Name, StatusServer.Name, StatusServerId); + /* + + foreach (var port in Ports) + { + // TODO JTA: This needs a better check + // you get a {"status": "success"} or "error": "error to register notification- Variable exists.." + if (!server.notify.parameters.Contains(port.Value.ResolutionVarID)) + { + RegisterForNotification(StatusServerId, port.Value.ResolutionVarID); + } + if (!server.notify.parameters.Contains(port.Value.ResolutionVarID)) + { + RegisterForNotification(StatusServerId, port.Value.SyncVarID); + } + } + */ + + break; + } + } + StatusServerId = null; + } + } + private void RegisterServerWithEndpoint() + { + /* + var registrationResult = RegisterServer(StatusServer.Address, string.Format("{0}-{1}", this.Name, StatusServer.Address), StatusServer.Server.Port.ToString()); + Debug.Console(2, this, "EvertzEndpointStatusServer Registration Result with device {0}\n{1}", Address, registrationResult); + if (registrationResult.Contains("success")) + { + RegisterStatusServer(); + } + else + { + Debug.Console(0, this, "EvertzEndpointStatusServer RegisterServerWithEndpoint with device {0}\n{1}", Address, registrationResult); + + } + * */ + } + + public void PollAll() + { + string collection = ""; + foreach (var parameter in Ports) + { + if (parameter.Key.Contains("@")) + { + collection = collection + parameter.Key + ","; + } + } + collection = collection.Substring(0, collection.Length - 1); + SendGetRequest(collection); + } + public void PollForState(string varId) + { + try + { + SendGetRequest(varId); + //var returnState = JsonConvert.DeserializeObject(SendGetRequest(varId)); + + } + catch (Exception e) + { + Debug.Console(0, this, "PollForState {0}", e); + + } + } + + + public void ProcessGetParameterResponse(EvertzPortRestResponse response) + { + var PortObject = Ports[response.id]; + if (response.name == "Input Status") + { + if (response.value == "Missing") { PortObject.SyncDetected = false; } + else { PortObject.SyncDetected = true; } + } + } + public void SendGetRequest(string s) + { + Client.SendText("v.api/apis/EV/GET/parameter/{0}", s); + } + + public void SendStatusRequest() + { + Client.SendText("/v.api/apis/EV/SERVERSTATUS"); + } + public void RegisterServer(string hostname, string servername, string port) + { + Client.SendText("v.api/apis/EV/SERVERADD/server/{0}/{1}/{2}/udp", hostname, servername, port); + } + public void UnregisterServer() + { + if (StatusServerId != null) + { + Client.SendTextNoResponse("v.api/apis/EV/SERVERDEL/server/{0}", StatusServerId); + } + } + + // TODO JTA: Craete a UnregisterServerFast using DispatchASync. + public void RegisterForNotification(string varId) + { + Client.SendText("v.api/apis/EV/NOTIFYADD/parameter/{0}/{1}", StatusServerId, varId); + } + + + void Client_ResponseRecived(object sender, GenericHttpClientEventArgs e) + { + if (e.Error == HTTP_CALLBACK_ERROR.COMPLETED) + { + if (e.RequestPath.Contains("GET/parameter/")) + { + // Get Parameter response + if (!e.ResponseText.Contains("[")) + ProcessGetParameterResponse(JsonConvert.DeserializeObject(e.ResponseText)); + else if (e.ResponseText.Contains("[")) + { + List test = JsonConvert.DeserializeObject>(e.ResponseText); + foreach (var thing in test) + { + ProcessGetParameterResponse(thing); + } + + } + } + else if (e.RequestPath.Contains("SERVERSTATUS")) + { + PollAll(); + ProcessServerStatusRequest(JsonConvert.DeserializeObject(e.ResponseText)); + } + } + } + + + + + public class EvertzPortsRestResponse + { + List test; + } + public class EvertzPortRestResponse + { + public string id; + public string name; + public string type; + public string value; + + } + + public class EvertzEndpointPort + { + public string PortName; + public string SyncVarID; + public string ResolutionVarID; + public bool SyncDetected; + public string Resolution; + public BoolFeedback SyncDetectedFeedback; + + public EvertzEndpointPort (string portName, string syncVarId, string resolutionVarId) + { + PortName = portName; + SyncVarID = syncVarId; + ResolutionVarID = resolutionVarId; + SyncDetectedFeedback = new BoolFeedback(() => { return SyncDetected; }); + } + + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpointPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpointPropertiesConfig.cs new file mode 100644 index 00000000..5beff4df --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpointPropertiesConfig.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common +{ + /// + /// + /// + public class EvertzEndpointPropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public ControlPropertiesConfig Control { get; set; } + public string userName { get; set; } + public string password { get; set; } + public string address { get; set; } + public string controllerKey { get; set; } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpointVarIds.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpointVarIds.cs new file mode 100644 index 00000000..fc5200f1 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/EvertzEndpointVarIds.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common +{ + public class EvertzEndpointVarIds + { + private string HdmiPort01SyncStatus = "136.0"; + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/GenericHttpClient.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/GenericHttpClient.cs new file mode 100644 index 00000000..53b4efd6 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Evertz/GenericHttpClient.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.Net.Http; +using PepperDash.Core; +using PepperDash.Core.DebugThings; + +namespace PepperDash.Essentials.Devices.Common +{ + public class GenericHttpClient : Device, IBasicCommunication + { + public HttpClient Client; + public event EventHandler ResponseRecived; + + public GenericHttpClient(string key, string name, string hostname) + : base(key, name) + { + Client = new HttpClient(); + Client.HostName = hostname; + + + } + + + /// + /// + /// + /// + public void SendText(string path) + { + HttpClientRequest request = new HttpClientRequest(); + string url = string.Format("http://{0}/{1}", Client.HostName, path); + request.Url = new UrlParser(url); + HttpClient.DISPATCHASYNC_ERROR error = Client.DispatchAsyncEx(request, Response, request); + Debug.Console(2, this, "GenericHttpClient SentRequest TX:'{0}'", url); + } + public void SendText(string format, params object[] items) + { + HttpClientRequest request = new HttpClientRequest(); + string url = string.Format("http://{0}/{1}", Client.HostName, string.Format(format, items)); + request.Url = new UrlParser(url); + HttpClient.DISPATCHASYNC_ERROR error = Client.DispatchAsyncEx(request, Response, request); + Debug.Console(2, this, "GenericHttpClient SentRequest TX:'{0}'", url); + } + + public void SendTextNoResponse(string format, params object[] items) + { + HttpClientRequest request = new HttpClientRequest(); + string url = string.Format("http://{0}/{1}", Client.HostName, string.Format(format, items)); + request.Url = new UrlParser(url); + Client.Dispatch(request); + Debug.Console(2, this, "GenericHttpClient SentRequest TX:'{0}'", url); + } + private void Response(HttpClientResponse response, HTTP_CALLBACK_ERROR error, object request) + { + if (error == HTTP_CALLBACK_ERROR.COMPLETED) + { + var responseReceived = response; + + if (responseReceived.ContentString.Length > 0) + { + if (ResponseRecived != null) + ResponseRecived(this, new GenericHttpClientEventArgs(responseReceived.ContentString, (request as HttpClientRequest).Url.ToString(), error)); + + Debug.Console(2, this, "GenericHttpClient ResponseReceived"); + Debug.Console(2, this, "RX:{0}", responseReceived.ContentString); + Debug.Console(2, this, "TX:{0}", (request as HttpClientRequest).Url.ToString()); + } + } + + } + + + #region IBasicCommunication Members + + public void SendBytes(byte[] bytes) + { + throw new NotImplementedException(); + } + + + + #endregion + + #region ICommunicationReceiver Members + + public event EventHandler BytesReceived; + + public void Connect() + { + throw new NotImplementedException(); + } + + public void Disconnect() + { + throw new NotImplementedException(); + } + + public bool IsConnected + { + get { return true; } + } + + public event EventHandler TextReceived; + + #endregion + } + public class GenericHttpClientEventArgs : EventArgs + { + public string ResponseText { get; private set; } + public string RequestPath { get; private set; } + public HTTP_CALLBACK_ERROR Error { get; set; } + public GenericHttpClientEventArgs(string response, string request, HTTP_CALLBACK_ERROR error) + { + ResponseText = response; + RequestPath = request; + Error = error; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Factory/DeviceFactory.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Factory/DeviceFactory.cs new file mode 100644 index 00000000..9c581c40 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Factory/DeviceFactory.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.GeneralIO; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.CrestronIO; + +using PepperDash.Essentials.Devices.Common; +using PepperDash.Essentials.Devices.Common.DSP; +using PepperDash.Essentials.Devices.Common.VideoCodec; +using PepperDash.Essentials.Devices.Common.Occupancy; +using PepperDash.Essentials.Devices.Common.Environment; + + + +namespace PepperDash.Essentials.Devices.Common +{ + public class DeviceFactory + { + public static IKeyed GetDevice(DeviceConfig dc) + { + var key = dc.Key; + var name = dc.Name; + var type = dc.Type; + var properties = dc.Properties; + var propAnon = new {}; + JsonConvert.DeserializeAnonymousType(dc.Properties.ToString(), propAnon); + + var typeName = dc.Type.ToLower(); + var groupName = dc.Group.ToLower(); + + if (typeName == "appletv") + { + var irCont = IRPortHelper.GetIrOutputPortController(dc); + return new AppleTV(key, name, irCont); + } + else if (typeName == "analogwaylivecore") + { + var comm = CommFactory.CreateCommForDevice(dc); + var props = JsonConvert.DeserializeObject( + properties.ToString()); + return new AnalogWayLiveCore(key, name, comm, props); + } + else if (typeName == "basicirdisplay") + { + var ir = IRPortHelper.GetIrPort(properties); + if (ir != null) + { + var display = new BasicIrDisplay(key, name, ir.Port, ir.FileName); + display.IrPulseTime = 200; // Set default pulse time for IR commands. + return display; + } + } + + else if (typeName == "biamptesira") + { + var comm = CommFactory.CreateCommForDevice(dc); + var props = JsonConvert.DeserializeObject( + properties.ToString()); + return new BiampTesiraForteDsp(key, name, comm, props); + } + + + else if (typeName == "cameravisca") + { + var comm = CommFactory.CreateCommForDevice(dc); + var props = JsonConvert.DeserializeObject( + properties.ToString()); + return new Cameras.CameraVisca(key, name, comm, props); + } + + else if (typeName == "cenrfgwex") + { + return CenRfgwController.GetNewExGatewayController(key, name, + properties.Value("id"), properties.Value("gatewayType")); + } + + else if (typeName == "cenerfgwpoe") + { + return CenRfgwController.GetNewErGatewayController(key, name, + properties.Value("id"), properties.Value("gatewayType")); + } + + else if (groupName == "discplayer") // (typeName == "irbluray") + { + if (properties["control"]["method"].Value() == "ir") + { + var irCont = IRPortHelper.GetIrOutputPortController(dc); + return new IRBlurayBase(key, name, irCont); + } + else if (properties["control"]["method"].Value() == "com") + { + Debug.Console(0, "[{0}] COM Device type not implemented YET!", key); + } + } + else if (typeName == "digitallogger") + { + // var comm = CommFactory.CreateCommForDevice(dc); + var props = JsonConvert.DeserializeObject( + properties.ToString()); + return new DigitalLogger(key, name, props); + } + else if (groupName == "evertzendpoint") + { + // var comm = CommFactory.CreateCommForDevice(dc); + var props = JsonConvert.DeserializeObject( + properties.ToString()); + return new EvertzEndpoint(key, name, props, typeName); + } + else if (typeName == "evertzendpointstatusserver") + { + var server = CommFactory.CreateCommForDevice(dc) as GenericUdpServer; + + var props = JsonConvert.DeserializeObject( + properties.ToString()); + return new EvertzEndpointStatusServer(key, name, server, props); + } + else if (typeName == "genericaudiooutwithvolume") + { + var zone = dc.Properties.Value("zone"); + return new GenericAudioOutWithVolume(key, name, + dc.Properties.Value("volumeDeviceKey"), zone); + } + + else if (groupName == "genericsource") + { + return new GenericSource(key, name); + } + + else if (typeName == "inroompc") + { + return new InRoomPc(key, name); + } + + else if (typeName == "laptop") + { + return new Laptop(key, name); + } + + else if (typeName == "mockvc") + { + return new VideoCodec.MockVC(dc); + } + + else if (typeName == "mockac") + { + var props = JsonConvert.DeserializeObject(properties.ToString()); + return new AudioCodec.MockAC(key, name, props); + } + + else if (typeName.StartsWith("ciscospark")) + { + var comm = CommFactory.CreateCommForDevice(dc); + return new VideoCodec.Cisco.CiscoSparkCodec(dc, comm); + } + + else if (typeName == "zoomroom") + { + var comm = CommFactory.CreateCommForDevice(dc); + return new VideoCodec.ZoomRoom.ZoomRoom(dc, comm); + } + + else if (typeName == "digitalinput") + { + var props = JsonConvert.DeserializeObject(properties.ToString()); + + IDigitalInputPorts portDevice; + + if (props.PortDeviceKey == "processor") + portDevice = Global.ControlSystem as IDigitalInputPorts; + else + portDevice = DeviceManager.GetDeviceForKey(props.PortDeviceKey) as IDigitalInputPorts; + + if (portDevice == null) + Debug.Console(0, "ERROR: Unable to add digital input device with key '{0}'. Port Device does not support digital inputs", key); + else + { + var cs = (portDevice as CrestronControlSystem); + if (cs == null) + { + Debug.Console(0, "ERROR: Port device for [{0}] is not control system", props.PortDeviceKey); + return null; + } + + if (cs.SupportsVersiport) + { + Debug.Console(1, "Attempting to add Digital Input device to Versiport port '{0}'", props.PortNumber); + + if (props.PortNumber > cs.NumberOfVersiPorts) + { + Debug.Console(0, "WARNING: Cannot add Vesiport {0} on {1}. Out of range", + props.PortNumber, props.PortDeviceKey); + return null; + } + + Versiport vp = cs.VersiPorts[props.PortNumber]; + + if (!vp.Registered) + { + var regSuccess = vp.Register(); + if (regSuccess == eDeviceRegistrationUnRegistrationResponse.Success) + { + Debug.Console(1, "Successfully Created Digital Input Device on Versiport"); + return new GenericVersiportDigitalInputDevice(key, vp, props); + } + else + { + Debug.Console(0, "WARNING: Attempt to register versiport {0} on device with key '{1}' failed: {2}", + props.PortNumber, props.PortDeviceKey, regSuccess); + return null; + } + } + } + else if (cs.SupportsDigitalInput) + { + Debug.Console(1, "Attempting to add Digital Input device to Digital Input port '{0}'", props.PortNumber); + + if (props.PortNumber > cs.NumberOfDigitalInputPorts) + { + Debug.Console(0, "WARNING: Cannot register DIO port {0} on {1}. Out of range", + props.PortNumber, props.PortDeviceKey); + return null; + } + + DigitalInput digitalInput = cs.DigitalInputPorts[props.PortNumber]; + + if (!digitalInput.Registered) + { + if (digitalInput.Register() == eDeviceRegistrationUnRegistrationResponse.Success) + { + Debug.Console(1, "Successfully Created Digital Input Device on Digital Input"); + return new GenericDigitalInputDevice(key, digitalInput); + } + else + Debug.Console(0, "WARNING: Attempt to register digital input {0} on device with key '{1}' failed.", + props.PortNumber, props.PortDeviceKey); + } + } + } + } + + else if (typeName == "relayoutput") + { + var props = JsonConvert.DeserializeObject(properties.ToString()); + + IRelayPorts portDevice; + + if (props.PortDeviceKey == "processor") + portDevice = Global.ControlSystem as IRelayPorts; + else + portDevice = DeviceManager.GetDeviceForKey(props.PortDeviceKey) as IRelayPorts; + + if (portDevice == null) + Debug.Console(0, "Unable to add relay device with key '{0}'. Port Device does not support relays", key); + else + { + var cs = (portDevice as CrestronControlSystem); + + if (cs != null) + { + // The relay is on a control system processor + if (!cs.SupportsRelay || props.PortNumber > cs.NumberOfRelayPorts) + { + Debug.Console(0, "Port Device: {0} does not support relays or does not have enough relays"); + return null; + } + } + else + { + // The relay is on another device type + + if (props.PortNumber > portDevice.NumberOfRelayPorts) + { + Debug.Console(0, "Port Device: {0} does not have enough relays"); + return null; + } + } + + Relay relay = portDevice.RelayPorts[props.PortNumber]; + + if (!relay.Registered) + { + if (relay.Register() == eDeviceRegistrationUnRegistrationResponse.Success) + return new GenericRelayDevice(key, relay); + else + Debug.Console(0, "Attempt to register relay {0} on device with key '{1}' failed.", props.PortNumber, props.PortDeviceKey); + } + + // Future: Check if portDevice is 3-series card or other non control system that supports versiports + } + } + + else if (typeName == "microphoneprivacycontroller") + { + var props = JsonConvert.DeserializeObject(properties.ToString()); + + return new Microphones.MicrophonePrivacyController(key, props); + } + else if (typeName == "roku") + { + var irCont = IRPortHelper.GetIrOutputPortController(dc); + return new Roku2(key, name, irCont); + } + + else if (groupName == "settopbox") //(typeName == "irstbbase") + { + var irCont = IRPortHelper.GetIrOutputPortController(dc); + var config = dc.Properties.ToObject(); + var stb = new IRSetTopBoxBase(key, name, irCont, config); + + //stb.HasDvr = properties.Value("hasDvr"); + var listName = properties.Value("presetsList"); + if (listName != null) + stb.LoadPresets(listName); + return stb; + } + else if (typeName == "tvonecorio") + { + var comm = CommFactory.CreateCommForDevice(dc); + var props = JsonConvert.DeserializeObject( + properties.ToString()); + return new TVOneCorio(key, name, comm, props); + } + + + else if (typeName == "glsoirccn") + { + var comm = CommFactory.GetControlPropertiesConfig(dc); + + GlsOccupancySensorBase occSensor = null; + + occSensor = new GlsOirCCn(comm.CresnetIdInt, Global.ControlSystem); + + if (occSensor != null) + return new EssentialsGlsOccupancySensorBaseController(key, name, occSensor); + else + Debug.Console(0, "ERROR: Unable to create Occupancy Sensor Device. Key: '{0}'", key); + } + + else if (typeName == "glsodtccn") + { + var comm = CommFactory.GetControlPropertiesConfig(dc); + + GlsOccupancySensorBase occSensor = null; + + occSensor = new GlsOdtCCn(comm.CresnetIdInt, Global.ControlSystem); + + if (occSensor != null) + return new EssentialsGlsOccupancySensorBaseController(key, name, occSensor); + else + Debug.Console(0, "ERROR: Unable to create Occupancy Sensor Device. Key: '{0}'", key); + } + + else if (groupName == "lighting") + { + if (typeName == "lutronqs") + { + var comm = CommFactory.CreateCommForDevice(dc); + + var props = JsonConvert.DeserializeObject(properties.ToString()); + + return new Environment.Lutron.LutronQuantumArea(key, name, comm, props); + } + else if (typeName == "din8sw8") + { + var comm = CommFactory.GetControlPropertiesConfig(dc); + + return new Environment.Lighting.Din8sw8Controller(key, comm.CresnetIdInt); + } + + } + + else if (groupName == "environment") + { + if (typeName == "shadecontroller") + { + var props = JsonConvert.DeserializeObject(properties.ToString()); + + return new Core.Shades.ShadeController(key, name, props); + } + else if (typeName == "relaycontrolledshade") + { + var props = JsonConvert.DeserializeObject(properties.ToString()); + + return new Environment.Somfy.RelayControlledShade(key, name, props); + } + + } + //else if (typeName == "qscdsp") + //{ + // var comm = CommFactory.CreateCommForDevice(dc); + // var props = JsonConvert.DeserializeObject( + // properties.ToString()); + // return new QscDsp(key, name, comm, props); + //} + + return null; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Generic/GenericSource.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Generic/GenericSource.cs new file mode 100644 index 00000000..04fae6a2 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Generic/GenericSource.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Common +{ + public class GenericSource : Device, IUiDisplayInfo, IRoutingOutputs, IUsageTracking + { + + public uint DisplayUiType { get { return DisplayUiConstants.TypeNoControls; } } + + public GenericSource(string key, string name) + : base(key, name) + { + + AnyOut = new RoutingOutputPort(RoutingPortNames.AnyOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + OutputPorts = new RoutingPortCollection { AnyOut }; + } + + #region IRoutingOutputs Members + + public RoutingOutputPort AnyOut { get; private set; } + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + #region IUsageTracking Members + + public UsageTracking UsageTracker { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/AnalogWay/AnalogWayLiveCorePropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/AnalogWay/AnalogWayLiveCorePropertiesConfig.cs new file mode 100644 index 00000000..a7055310 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/AnalogWay/AnalogWayLiveCorePropertiesConfig.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common +{ + /// + /// + /// + public class AnalogWayLiveCorePropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public ControlPropertiesConfig Control { get; set; } + public string userName { get; set; } + public string password { get; set; } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/AnalogWay/AnalongWayLiveCore.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/AnalogWay/AnalongWayLiveCore.cs new file mode 100644 index 00000000..c2178bdb --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/AnalogWay/AnalongWayLiveCore.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System.Text.RegularExpressions; + + +namespace PepperDash.Essentials.Devices.Common +{ + + public class AnalogWayLiveCore : Device + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + public string userName; + public string password; + private bool OnlineStatus; + public BoolFeedback OnlineFeedback; + private ushort CurrentPreset; + public IntFeedback PresetFeedback; + + // new public Dictionary LevelControlPoints { get; private set; } + // public List PresetList = new List(); + + public bool isSubscribed; + + private CTimer SubscriptionTimer; + + CrestronQueue CommandQueue; + + bool CommandQueueInProgress = false; + + //new public Dictionary DialerControlPoints { get; private set; } + + //new public Dictionary SwitcherControlPoints { get; private set; } + + /// + /// Shows received lines as hex + /// + public bool ShowHexResponse { get; set; } + + public AnalogWayLiveCore(string key, string name, IBasicCommunication comm, AnalogWayLiveCorePropertiesConfig props) : + base(key, name) + { + + this.userName = props.userName; + this.password = props.password; + CommandQueue = new CrestronQueue(100); + + + Communication = comm; + + var socket = comm as ISocketStatus; + if (socket != null) + { + // This instance uses IP control + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + else + { + // This instance uses RS-232 control + } + PortGather = new CommunicationGather(Communication, "\x0a"); + PortGather.LineReceived += this.Port_LineReceived; + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { + //#warning Need to deal with this poll string + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 120000, 120000, 300000, "System.Status\x0A\x0D"); + } + + } + + public override bool CustomActivate() + { + + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + OnlineFeedback = new BoolFeedback(() => { return OnlineStatus; }); + PresetFeedback = new IntFeedback(() => { return CurrentPreset; }); + + CrestronConsole.AddNewConsoleCommand(SendLine, "send" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + return true; + } + + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.Console(2, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); + + if (e.Client.IsConnected) + { + OnlineStatus = true; + OnlineFeedback.FireUpdate(); + } + else + { + OnlineStatus = false; + OnlineFeedback.FireUpdate(); + if (SubscriptionTimer != null) + { + SubscriptionTimer.Stop(); + SubscriptionTimer = null; + } + + isSubscribed = false; + CommandQueue.Clear(); + CommandQueueInProgress = false; + } + } + + /// + /// Initiates the subscription process to the DSP + /// + + + + + /// + /// Handles a response message from the DSP + /// + /// + /// + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + Debug.Console(2, this, "TVOneCurio RX: '{0}'", args.Text); + try + { + if (args.Text.IndexOf("login") > -1) + { + SendLine(string.Format("Login({0},{1})", this.userName, this.password)); + } + else if (args.Text.IndexOf("!Done Preset.Take =") > -1) + { + + string presetNumberParse = args.Text.Remove(0, args.Text.IndexOf("=") + 2); + + Debug.Console(1, this, "Preset Parse: {0}", presetNumberParse); + CurrentPreset = ushort.Parse(presetNumberParse); + PresetFeedback.FireUpdate(); + + + } + + + } + catch (Exception e) + { + if (Debug.Level == 2) + Debug.Console(2, this, "Error parsing response: '{0}'\n{1}", args.Text, e); + } + + } + + /// + /// Sends a command to the DSP (with delimiter appended) + /// + /// Command to send + public void SendLine(string s) + { + Debug.Console(1, this, "TVOne Cusio TX: '{0}'", s); + Communication.SendText(s + "\x0d\x0a"); + } + + /// + /// Adds a command from a child module to the queue + /// + /// Command object from child module + public void EnqueueCommand(QueuedCommand commandToEnqueue) + { + CommandQueue.Enqueue(commandToEnqueue); + //Debug.Console(1, this, "Command (QueuedCommand) Enqueued '{0}'. CommandQueue has '{1}' Elements.", commandToEnqueue.Command, CommandQueue.Count); + + if(!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Adds a raw string command to the queue + /// + /// + public void EnqueueCommand(string command) + { + CommandQueue.Enqueue(command); + //Debug.Console(1, this, "Command (string) Enqueued '{0}'. CommandQueue has '{1}' Elements.", command, CommandQueue.Count); + + if (!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Sends the next queued command to the DSP + /// + void SendNextQueuedCommand() + { + if (Communication.IsConnected && !CommandQueue.IsEmpty) + { + CommandQueueInProgress = true; + + if (CommandQueue.Peek() is QueuedCommand) + { + QueuedCommand nextCommand = new QueuedCommand(); + + nextCommand = (QueuedCommand)CommandQueue.Peek(); + + SendLine(nextCommand.Command); + } + else + { + string nextCommand = (string)CommandQueue.Peek(); + + SendLine(nextCommand); + } + } + + } + + + public void CallPreset(ushort presetNumber) + { + SendLine(string.Format("Preset.Take = {0}", presetNumber)); + // SendLine("cgp 1"); + } + + public class QueuedCommand + { + public string Command { get; set; } + public string AttributeCode { get; set; } + // public QscDspControlPoint ControlPoint { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOne/TVOneCorio.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOne/TVOneCorio.cs new file mode 100644 index 00000000..a3dd9745 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOne/TVOneCorio.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System.Text.RegularExpressions; + + +namespace PepperDash.Essentials.Devices.Common +{ + + public class TVOneCorio : Device + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + public string userName; + public string password; + private bool OnlineStatus; + public BoolFeedback OnlineFeedback; + private ushort CurrentPreset; + public IntFeedback PresetFeedback; + + // new public Dictionary LevelControlPoints { get; private set; } + // public List PresetList = new List(); + + public bool isSubscribed; + + private CTimer SubscriptionTimer; + + CrestronQueue CommandQueue; + + bool CommandQueueInProgress = false; + + //new public Dictionary DialerControlPoints { get; private set; } + + //new public Dictionary SwitcherControlPoints { get; private set; } + + /// + /// Shows received lines as hex + /// + public bool ShowHexResponse { get; set; } + + public TVOneCorio(string key, string name, IBasicCommunication comm, TVOneCorioPropertiesConfig props) : + base(key, name) + { + + this.userName = props.userName; + this.password = props.password; + CommandQueue = new CrestronQueue(100); + + + Communication = comm; + + var socket = comm as ISocketStatus; + if (socket != null) + { + // This instance uses IP control + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + else + { + // This instance uses RS-232 control + } + PortGather = new CommunicationGather(Communication, "\x0a"); + PortGather.LineReceived += this.Port_LineReceived; + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { + //#warning Need to deal with this poll string + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 120000, 120000, 300000, "System.Status\x0A\x0D"); + } + + } + + public override bool CustomActivate() + { + + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + OnlineFeedback = new BoolFeedback(() => { return OnlineStatus; }); + PresetFeedback = new IntFeedback(() => { return CurrentPreset; }); + + CrestronConsole.AddNewConsoleCommand(SendLine, "send" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + return true; + } + + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.Console(2, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); + + if (e.Client.IsConnected) + { + OnlineStatus = true; + OnlineFeedback.FireUpdate(); + } + else + { + OnlineStatus = false; + OnlineFeedback.FireUpdate(); + if (SubscriptionTimer != null) + { + SubscriptionTimer.Stop(); + SubscriptionTimer = null; + } + + isSubscribed = false; + CommandQueue.Clear(); + CommandQueueInProgress = false; + } + } + + /// + /// Initiates the subscription process to the DSP + /// + + + + + /// + /// Handles a response message from the DSP + /// + /// + /// + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + Debug.Console(2, this, "TVOneCurio RX: '{0}'", args.Text); + try + { + if (args.Text.IndexOf("login") > -1) + { + SendLine(string.Format("Login({0},{1})", this.userName, this.password)); + } + else if (args.Text.IndexOf("!Done Preset.Take =") > -1) + { + + string presetNumberParse = args.Text.Remove(0, args.Text.IndexOf("=") + 2); + + Debug.Console(1, this, "Preset Parse: {0}", presetNumberParse); + CurrentPreset = ushort.Parse(presetNumberParse); + PresetFeedback.FireUpdate(); + + + } + + + } + catch (Exception e) + { + if (Debug.Level == 2) + Debug.Console(2, this, "Error parsing response: '{0}'\n{1}", args.Text, e); + } + + } + + /// + /// Sends a command to the DSP (with delimiter appended) + /// + /// Command to send + public void SendLine(string s) + { + Debug.Console(1, this, "TVOne Cusio TX: '{0}'", s); + Communication.SendText(s + "\x0d\x0a"); + } + + /// + /// Adds a command from a child module to the queue + /// + /// Command object from child module + public void EnqueueCommand(QueuedCommand commandToEnqueue) + { + CommandQueue.Enqueue(commandToEnqueue); + //Debug.Console(1, this, "Command (QueuedCommand) Enqueued '{0}'. CommandQueue has '{1}' Elements.", commandToEnqueue.Command, CommandQueue.Count); + + if(!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Adds a raw string command to the queue + /// + /// + public void EnqueueCommand(string command) + { + CommandQueue.Enqueue(command); + //Debug.Console(1, this, "Command (string) Enqueued '{0}'. CommandQueue has '{1}' Elements.", command, CommandQueue.Count); + + if (!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Sends the next queued command to the DSP + /// + void SendNextQueuedCommand() + { + if (Communication.IsConnected && !CommandQueue.IsEmpty) + { + CommandQueueInProgress = true; + + if (CommandQueue.Peek() is QueuedCommand) + { + QueuedCommand nextCommand = new QueuedCommand(); + + nextCommand = (QueuedCommand)CommandQueue.Peek(); + + SendLine(nextCommand.Command); + } + else + { + string nextCommand = (string)CommandQueue.Peek(); + + SendLine(nextCommand); + } + } + + } + + + public void CallPreset(ushort presetNumber) + { + SendLine(string.Format("Preset.Take = {0}", presetNumber)); + // SendLine("cgp 1"); + } + + public class QueuedCommand + { + public string Command { get; set; } + public string AttributeCode { get; set; } + // public QscDspControlPoint ControlPoint { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOne/TVOneCorioPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOne/TVOneCorioPropertiesConfig.cs new file mode 100644 index 00000000..410fdb27 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOne/TVOneCorioPropertiesConfig.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common +{ + /// + /// + /// + public class TVOneCorioPropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public ControlPropertiesConfig Control { get; set; } + public string userName { get; set; } + public string password { get; set; } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOneCorio.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOneCorio.cs new file mode 100644 index 00000000..a3dd9745 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOneCorio.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System.Text.RegularExpressions; + + +namespace PepperDash.Essentials.Devices.Common +{ + + public class TVOneCorio : Device + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + public string userName; + public string password; + private bool OnlineStatus; + public BoolFeedback OnlineFeedback; + private ushort CurrentPreset; + public IntFeedback PresetFeedback; + + // new public Dictionary LevelControlPoints { get; private set; } + // public List PresetList = new List(); + + public bool isSubscribed; + + private CTimer SubscriptionTimer; + + CrestronQueue CommandQueue; + + bool CommandQueueInProgress = false; + + //new public Dictionary DialerControlPoints { get; private set; } + + //new public Dictionary SwitcherControlPoints { get; private set; } + + /// + /// Shows received lines as hex + /// + public bool ShowHexResponse { get; set; } + + public TVOneCorio(string key, string name, IBasicCommunication comm, TVOneCorioPropertiesConfig props) : + base(key, name) + { + + this.userName = props.userName; + this.password = props.password; + CommandQueue = new CrestronQueue(100); + + + Communication = comm; + + var socket = comm as ISocketStatus; + if (socket != null) + { + // This instance uses IP control + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + else + { + // This instance uses RS-232 control + } + PortGather = new CommunicationGather(Communication, "\x0a"); + PortGather.LineReceived += this.Port_LineReceived; + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { + //#warning Need to deal with this poll string + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 120000, 120000, 300000, "System.Status\x0A\x0D"); + } + + } + + public override bool CustomActivate() + { + + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + OnlineFeedback = new BoolFeedback(() => { return OnlineStatus; }); + PresetFeedback = new IntFeedback(() => { return CurrentPreset; }); + + CrestronConsole.AddNewConsoleCommand(SendLine, "send" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + return true; + } + + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.Console(2, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); + + if (e.Client.IsConnected) + { + OnlineStatus = true; + OnlineFeedback.FireUpdate(); + } + else + { + OnlineStatus = false; + OnlineFeedback.FireUpdate(); + if (SubscriptionTimer != null) + { + SubscriptionTimer.Stop(); + SubscriptionTimer = null; + } + + isSubscribed = false; + CommandQueue.Clear(); + CommandQueueInProgress = false; + } + } + + /// + /// Initiates the subscription process to the DSP + /// + + + + + /// + /// Handles a response message from the DSP + /// + /// + /// + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + Debug.Console(2, this, "TVOneCurio RX: '{0}'", args.Text); + try + { + if (args.Text.IndexOf("login") > -1) + { + SendLine(string.Format("Login({0},{1})", this.userName, this.password)); + } + else if (args.Text.IndexOf("!Done Preset.Take =") > -1) + { + + string presetNumberParse = args.Text.Remove(0, args.Text.IndexOf("=") + 2); + + Debug.Console(1, this, "Preset Parse: {0}", presetNumberParse); + CurrentPreset = ushort.Parse(presetNumberParse); + PresetFeedback.FireUpdate(); + + + } + + + } + catch (Exception e) + { + if (Debug.Level == 2) + Debug.Console(2, this, "Error parsing response: '{0}'\n{1}", args.Text, e); + } + + } + + /// + /// Sends a command to the DSP (with delimiter appended) + /// + /// Command to send + public void SendLine(string s) + { + Debug.Console(1, this, "TVOne Cusio TX: '{0}'", s); + Communication.SendText(s + "\x0d\x0a"); + } + + /// + /// Adds a command from a child module to the queue + /// + /// Command object from child module + public void EnqueueCommand(QueuedCommand commandToEnqueue) + { + CommandQueue.Enqueue(commandToEnqueue); + //Debug.Console(1, this, "Command (QueuedCommand) Enqueued '{0}'. CommandQueue has '{1}' Elements.", commandToEnqueue.Command, CommandQueue.Count); + + if(!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Adds a raw string command to the queue + /// + /// + public void EnqueueCommand(string command) + { + CommandQueue.Enqueue(command); + //Debug.Console(1, this, "Command (string) Enqueued '{0}'. CommandQueue has '{1}' Elements.", command, CommandQueue.Count); + + if (!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Sends the next queued command to the DSP + /// + void SendNextQueuedCommand() + { + if (Communication.IsConnected && !CommandQueue.IsEmpty) + { + CommandQueueInProgress = true; + + if (CommandQueue.Peek() is QueuedCommand) + { + QueuedCommand nextCommand = new QueuedCommand(); + + nextCommand = (QueuedCommand)CommandQueue.Peek(); + + SendLine(nextCommand.Command); + } + else + { + string nextCommand = (string)CommandQueue.Peek(); + + SendLine(nextCommand); + } + } + + } + + + public void CallPreset(ushort presetNumber) + { + SendLine(string.Format("Preset.Take = {0}", presetNumber)); + // SendLine("cgp 1"); + } + + public class QueuedCommand + { + public string Command { get; set; } + public string AttributeCode { get; set; } + // public QscDspControlPoint ControlPoint { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOneCorioPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOneCorioPropertiesConfig.cs new file mode 100644 index 00000000..410fdb27 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/ImageProcessors/TVOneCorioPropertiesConfig.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common +{ + /// + /// + /// + public class TVOneCorioPropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public ControlPropertiesConfig Control { get; set; } + public string userName { get; set; } + public string password { get; set; } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/MIcrophone/MicrophonePrivacyController.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/MIcrophone/MicrophonePrivacyController.cs new file mode 100644 index 00000000..177fffd1 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/MIcrophone/MicrophonePrivacyController.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.CrestronIO; + + +namespace PepperDash.Essentials.Devices.Common.Microphones +{ + /// + /// Used for applications where one or more microphones with momentary contact closure outputs are used to + /// toggle the privacy state of the room. Privacy state feedback is represented + /// + public class MicrophonePrivacyController : Device + { + MicrophonePrivacyControllerConfig Config; + + bool initialized; + + public bool EnableLeds + { + get + { + return _enableLeds; + } + set + { + _enableLeds = value; + + if (initialized) + { + if (value) + { + CheckPrivacyMode(); + SetLedStates(); + } + else + TurnOffAllLeds(); + } + } + } + bool _enableLeds; + + public List Inputs { get; private set; } + + public GenericRelayDevice RedLedRelay { get; private set; } + bool _redLedRelayState; + + public GenericRelayDevice GreenLedRelay { get; private set; } + bool _greenLedRelayState; + + public IPrivacy PrivacyDevice { get; private set; } + + public MicrophonePrivacyController(string key, MicrophonePrivacyControllerConfig config) : + base(key) + { + Config = config; + + Inputs = new List(); + } + + public override bool CustomActivate() + { + foreach (var i in Config.Inputs) + { + var input = DeviceManager.GetDeviceForKey(i.DeviceKey) as IDigitalInput; + + if(input != null) + AddInput(input); + } + + var greenLed = DeviceManager.GetDeviceForKey(Config.GreenLedRelay.DeviceKey) as GenericRelayDevice; + + if (greenLed != null) + GreenLedRelay = greenLed; + else + Debug.Console(0, this, "Unable to add Green LED device"); + + var redLed = DeviceManager.GetDeviceForKey(Config.RedLedRelay.DeviceKey) as GenericRelayDevice; + + if (redLed != null) + RedLedRelay = redLed; + else + Debug.Console(0, this, "Unable to add Red LED device"); + + CheckPrivacyMode(); + + initialized = true; + + return base.CustomActivate(); + } + + public void SetPrivacyDevice(IPrivacy privacyDevice) + { + PrivacyDevice = privacyDevice; + + PrivacyDevice.PrivacyModeIsOnFeedback.OutputChange += PrivacyModeIsOnFeedback_OutputChange; + } + + void PrivacyModeIsOnFeedback_OutputChange(object sender, EventArgs e) + { + Debug.Console(1, this, "Privacy mode change: {0}", sender as BoolFeedback); + CheckPrivacyMode(); + } + + void CheckPrivacyMode() + { + if (PrivacyDevice != null) + { + var privacyState = PrivacyDevice.PrivacyModeIsOnFeedback.BoolValue; + + if (privacyState) + TurnOnRedLeds(); + else + TurnOnGreenLeds(); + } + } + + void AddInput(IDigitalInput input) + { + Inputs.Add(input); + + input.InputStateFeedback.OutputChange += InputStateFeedback_OutputChange; + } + + void RemoveInput(IDigitalInput input) + { + var tempInput = Inputs.FirstOrDefault(i => i.Equals(input)); + + if (tempInput != null) + tempInput.InputStateFeedback.OutputChange -= InputStateFeedback_OutputChange; + + Inputs.Remove(input); + } + + void SetRedLedRelay(GenericRelayDevice relay) + { + RedLedRelay = relay; + } + + void SetGreenLedRelay(GenericRelayDevice relay) + { + GreenLedRelay = relay; + } + + /// + /// Check the state of the input change and handle accordingly + /// + /// + /// + void InputStateFeedback_OutputChange(object sender, EventArgs e) + { + if ((sender as BoolFeedback).BoolValue == true) + TogglePrivacyMute(); + } + + /// + /// Toggles the state of the privacy mute + /// + public void TogglePrivacyMute() + { + PrivacyDevice.PrivacyModeToggle(); + } + + void TurnOnRedLeds() + { + _greenLedRelayState = false; + _redLedRelayState = true; + SetLedStates(); + } + + void TurnOnGreenLeds() + { + _redLedRelayState = false; + _greenLedRelayState = true; + SetLedStates(); + } + + /// + /// If enabled, sets the actual state of the relays + /// + void SetLedStates() + { + if (_enableLeds) + { + SetRelayStates(); + } + else + TurnOffAllLeds(); + } + + /// + /// Turns off all LEDs + /// + void TurnOffAllLeds() + { + _redLedRelayState = false; + _greenLedRelayState = false; + + SetRelayStates(); + } + + void SetRelayStates() + { + if (RedLedRelay != null) + { + if (_redLedRelayState) + RedLedRelay.CloseRelay(); + else + RedLedRelay.OpenRelay(); + } + + if(GreenLedRelay != null) + { + if (_greenLedRelayState) + GreenLedRelay.CloseRelay(); + else + GreenLedRelay.OpenRelay(); + } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/MIcrophone/MicrophonePrivacyControllerConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/MIcrophone/MicrophonePrivacyControllerConfig.cs new file mode 100644 index 00000000..238fc7cf --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/MIcrophone/MicrophonePrivacyControllerConfig.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core.CrestronIO; + +namespace PepperDash.Essentials.Devices.Common.Microphones +{ + public class MicrophonePrivacyControllerConfig + { + public List Inputs { get; set; } + public KeyedDevice GreenLedRelay { get; set; } + public KeyedDevice RedLedRelay { get; set; } + } + + public class KeyedDevice + { + public string DeviceKey { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsGlsOccupancySensorBaseController.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsGlsOccupancySensorBaseController.cs new file mode 100644 index 00000000..14892b9a --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsGlsOccupancySensorBaseController.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.GeneralIO; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.Occupancy +{ + public class EssentialsGlsOccupancySensorBaseController : CrestronGenericBaseDevice, IOccupancyStatusProvider + { + public GlsOccupancySensorBase OccSensor { get; private set; } + + public BoolFeedback RoomIsOccupiedFeedback { get; private set; } + + // Debug properties + public bool InTestMode { get; private set; } + + public bool TestRoomIsOccupiedFeedback { get; private set; } + + public Func RoomIsOccupiedFeedbackFunc + { + get + { + return () => InTestMode ? TestRoomIsOccupiedFeedback : OccSensor.OccupancyDetectedFeedback.BoolValue; + } + } + + public EssentialsGlsOccupancySensorBaseController(string key, string name, GlsOccupancySensorBase sensor) + : base(key, name, sensor) + { + OccSensor = sensor; + + RoomIsOccupiedFeedback = new BoolFeedback(RoomIsOccupiedFeedbackFunc); + + OccSensor.BaseEvent += new Crestron.SimplSharpPro.BaseEventHandler(OccSensor_BaseEvent); + } + + void OccSensor_BaseEvent(Crestron.SimplSharpPro.GenericBase device, Crestron.SimplSharpPro.BaseEventArgs args) + { + Debug.Console(2, this, "GlsOccupancySensorChange EventId: {0}", args.EventId); + + if (args.EventId == Crestron.SimplSharpPro.GeneralIO.GlsOccupancySensorBase.RoomOccupiedFeedbackEventId + || args.EventId == Crestron.SimplSharpPro.GeneralIO.GlsOccupancySensorBase.RoomVacantFeedbackEventId) + { + Debug.Console(1, this, "Occupancy State: {0}", OccSensor.OccupancyDetectedFeedback.BoolValue); + RoomIsOccupiedFeedback.FireUpdate(); + } + } + + public void SetTestMode(bool mode) + { + InTestMode = mode; + + Debug.Console(1, this, "In Mock Mode: '{0}'", InTestMode); + } + + public void SetTestOccupiedState(bool state) + { + if (!InTestMode) + Debug.Console(1, "Mock mode not enabled"); + else + { + TestRoomIsOccupiedFeedback = state; + + RoomIsOccupiedFeedback.FireUpdate(); + } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsGlsOccupancySensorBaseController.cs.orig b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsGlsOccupancySensorBaseController.cs.orig new file mode 100644 index 00000000..f65e1b37 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsGlsOccupancySensorBaseController.cs.orig @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.GeneralIO; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.Occupancy +{ + public class EssentialsGlsOccupancySensorBaseController : CrestronGenericBaseDevice, IOccupancyStatusProvider + { + public GlsOccupancySensorBase OccSensor { get; private set; } + + public BoolFeedback RoomIsOccupiedFeedback { get; private set; } + +<<<<<<< HEAD + /// + /// Set by debugging functions + /// + public bool InMockMode { get; private set; } + + public bool MockRoomIsOccupiedFeedback { get; private set; } +======= + // Debug properties + public bool InTestMode { get; private set; } + + public bool TestRoomIsOccupiedFeedback { get; private set; } +>>>>>>> origin/feature/ecs-342-neil + + public Func RoomIsOccupiedFeedbackFunc + { + get + { +<<<<<<< HEAD + return () => InMockMode ? MockRoomIsOccupiedFeedback : OccSensor.OccupancyDetectedFeedback.BoolValue; +======= + return () => InTestMode ? TestRoomIsOccupiedFeedback : OccSensor.OccupancyDetectedFeedback.BoolValue; +>>>>>>> origin/feature/ecs-342-neil + } + } + + public EssentialsGlsOccupancySensorBaseController(string key, string name, GlsOccupancySensorBase sensor, GlsOccupancySensorConfigurationProperties props) + : base(key, name, sensor) + { + OccSensor = sensor; + RoomIsOccupiedFeedback = new BoolFeedback(RoomIsOccupiedFeedbackFunc); + + OccSensor.GlsOccupancySensorChange += new GlsOccupancySensorChangeEventHandler(sensor_GlsOccupancySensorChange); + } + + void sensor_GlsOccupancySensorChange(GlsOccupancySensorBase device, GlsOccupancySensorChangeEventArgs args) + { + RoomIsOccupiedFeedback.FireUpdate(); + } + + public void SetTestMode(bool mode) + { + InTestMode = mode; + + Debug.Console(1, this, "In Mock Mode: '{0}'", InTestMode); + } + + public void SetTestOccupiedState(bool state) + { + if (!InTestMode) + Debug.Console(1, "Mock mode not enabled"); + else + { + TestRoomIsOccupiedFeedback = state; + + RoomIsOccupiedFeedback.FireUpdate(); + } + } + } + + /// + /// + /// + public class GlsOccupancySensorConfigurationProperties + { + public string CresnetId { get; set; } + public string Model { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsOccupancyAggregator.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsOccupancyAggregator.cs new file mode 100644 index 00000000..e24d468c --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/EssentialsOccupancyAggregator.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.Occupancy +{ + /// + /// Aggregates the RoomIsOccupied feedbacks of one or more IOccupancyStatusProvider objects + /// + public class EssentialsOccupancyAggregator : Device, IOccupancyStatusProvider + { + /// + /// Aggregated feedback of all linked IOccupancyStatusProvider devices + /// + public BoolFeedback RoomIsOccupiedFeedback + { + get + { + return AggregatedOccupancyStatus.Output; + } + } + + private BoolFeedbackOr AggregatedOccupancyStatus; + + public EssentialsOccupancyAggregator(string key, string name) + : base(key, name) + { + AggregatedOccupancyStatus = new BoolFeedbackOr(); + } + + /// + /// Adds an IOccupancyStatusProvider device + /// + /// + public void AddOccupancyStatusProvider(IOccupancyStatusProvider statusProvider) + { + AggregatedOccupancyStatus.AddOutputIn(statusProvider.RoomIsOccupiedFeedback); + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/iOccupancyStatusProvider.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/iOccupancyStatusProvider.cs new file mode 100644 index 00000000..02054535 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Occupancy/iOccupancyStatusProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.Occupancy +{ + public interface IOccupancyStatusProvider + { + BoolFeedback RoomIsOccupiedFeedback { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/PC/InRoomPc.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/PC/InRoomPc.cs new file mode 100644 index 00000000..0e25ee0d --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/PC/InRoomPc.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharpPro; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Core; + +namespace PepperDash.Essentials.Devices.Common +{ + /// + /// This DVD class should cover most IR, one-way DVD and Bluray fuctions + /// + public class InRoomPc : Device, IHasFeedback, IRoutingOutputs, IAttachVideoStatus, IUiDisplayInfo, IUsageTracking + { + public uint DisplayUiType { get { return DisplayUiConstants.TypeLaptop; } } + public string IconName { get; set; } + public BoolFeedback HasPowerOnFeedback { get; private set; } + + public RoutingOutputPort AnyVideoOut { get; private set; } + + #region IRoutingOutputs Members + + /// + /// Options: hdmi + /// + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + public InRoomPc(string key, string name) + : base(key, name) + { + IconName = "PC"; + HasPowerOnFeedback = new BoolFeedback("HasPowerFeedback", + () => this.GetVideoStatuses() != VideoStatusOutputs.NoStatus); + OutputPorts = new RoutingPortCollection(); + OutputPorts.Add(AnyVideoOut = new RoutingOutputPort(RoutingPortNames.AnyVideoOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.None, 0, this)); + } + + #region IHasFeedback Members + + /// + /// Passes through the VideoStatuses list + /// + public FeedbackCollection Feedbacks + { + get + { + var newList = new FeedbackCollection(); + newList.AddRange(this.GetVideoStatuses().ToList()); + return newList; + } + } + + #endregion + + #region IUsageTracking Members + + public UsageTracking UsageTracker { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/PC/Laptop.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/PC/Laptop.cs new file mode 100644 index 00000000..3c9fc714 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/PC/Laptop.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharpPro; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Core; + +namespace PepperDash.Essentials.Devices.Common +{ + /// + /// This DVD class should cover most IR, one-way DVD and Bluray fuctions + /// + public class Laptop : Device, IHasFeedback, IRoutingOutputs, IAttachVideoStatus, IUiDisplayInfo, IUsageTracking + { + public uint DisplayUiType { get { return DisplayUiConstants.TypeLaptop; } } + public string IconName { get; set; } + public BoolFeedback HasPowerOnFeedback { get; private set; } + + public RoutingOutputPort AnyVideoOut { get; private set; } + + #region IRoutingOutputs Members + + /// + /// Options: hdmi + /// + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + public Laptop(string key, string name) + : base(key, name) + { + IconName = "Laptop"; + HasPowerOnFeedback = new BoolFeedback("HasPowerFeedback", + () => this.GetVideoStatuses() != VideoStatusOutputs.NoStatus); + OutputPorts = new RoutingPortCollection(); + OutputPorts.Add(AnyVideoOut = new RoutingOutputPort(RoutingPortNames.AnyOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.None, 0, this)); + } + + #region IHasFeedback Members + + /// + /// Passes through the VideoStatuses list + /// + public FeedbackCollection Feedbacks + { + get + { + var newList = new FeedbackCollection(); + newList.AddRange(this.GetVideoStatuses().ToList()); + return newList; + } + } + + #endregion + + #region IUsageTracking Members + + public UsageTracking UsageTracker { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Power Controllers/DigitalLoggerPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Power Controllers/DigitalLoggerPropertiesConfig.cs new file mode 100644 index 00000000..1fae442b --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Power Controllers/DigitalLoggerPropertiesConfig.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common +{ + /// + /// + /// + public class DigitalLoggerPropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public ControlPropertiesConfig Control { get; set; } + public string userName { get; set; } + public string password { get; set; } + public string address { get; set; } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Power Controllers/Digitallogger.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Power Controllers/Digitallogger.cs new file mode 100644 index 00000000..e48a5b7b --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Power Controllers/Digitallogger.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System.Text.RegularExpressions; +using Crestron.SimplSharp.Net.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + + +namespace PepperDash.Essentials.Devices.Common +{ + + public class DigitalLogger : Device + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + + private HttpClient WebClient; + public string userName; + public string password; + public string address; + private bool OnlineStatus; + public BoolFeedback OnlineFeedback; + private ushort CurrentPreset; + public IntFeedback PresetFeedback; + + public Dictionary CircuitStatus; + public uint CircuitCount; + + public Dictionary CircuitNameFeedbacks { get; private set; } + public Dictionary CircuitIsCritical{ get; private set; } + public Dictionary CircuitState { get; private set; } + + // new public Dictionary LevelControlPoints { get; private set; } + // public List PresetList = new List(); + + public bool isSubscribed; + + private CTimer SubscriptionTimer; + + CrestronQueue CommandQueue; + + bool CommandQueueInProgress = false; + + //new public Dictionary DialerControlPoints { get; private set; } + + //new public Dictionary SwitcherControlPoints { get; private set; } + + /// + /// Shows received lines as hex + /// + public bool ShowHexResponse { get; set; } + + public DigitalLogger(string key, string name, DigitalLoggerPropertiesConfig props) : + base(key, name) + { + CircuitCount = 8; + this.userName = props.userName; + this.password = props.password; + CommandQueue = new CrestronQueue(100); + + WebClient = new HttpClient(); + WebClient.UserName = this.userName; + WebClient.Password = this.password; + this.address = props.address; + WebClient.HostAddress = props.address; + + + + } + + public override bool CustomActivate() + { + /* + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + */ + + OnlineFeedback = new BoolFeedback(() => { return OnlineStatus; }); + CircuitStatus = new Dictionary(); + CircuitNameFeedbacks = new Dictionary(); + CircuitIsCritical = new Dictionary(); + CircuitState = new Dictionary(); + for (uint i = 0; i < CircuitCount; i++) + { + uint circuit = i; + CircuitStatus[circuit] = new DigitalLoggerCircuit(); + CircuitNameFeedbacks[circuit] = new StringFeedback(() => { + if (CircuitStatus[circuit].name != null) + { + return CircuitStatus[circuit].name; + } + else + { + return ""; + } + }); + CircuitIsCritical[circuit] = new BoolFeedback(() => + { + if (CircuitStatus[circuit].critical != null) + { + return CircuitStatus[circuit].critical; + } + else + { + return false; + } + }); + CircuitState[circuit] = new BoolFeedback(() => + { + if (CircuitStatus[circuit].state != null) + { + return CircuitStatus[circuit].state; + } + else + { + return false; + } + }); + PollCircuit(circuit); + } + + CrestronConsole.AddNewConsoleCommand(SendLine, "send" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + + + + return true; + } + + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.Console(2, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); + + if (e.Client.IsConnected) + { + OnlineStatus = true; + OnlineFeedback.FireUpdate(); + } + else + { + OnlineStatus = false; + OnlineFeedback.FireUpdate(); + if (SubscriptionTimer != null) + { + SubscriptionTimer.Stop(); + SubscriptionTimer = null; + } + + isSubscribed = false; + CommandQueue.Clear(); + CommandQueueInProgress = false; + } + } + + public void PollCircuit(uint circuit) + { + try + { + string PollCircuitResponse = SendRequest(String.Format("/restapi/relay/outlets/{0}/", circuit)); + CircuitStatus[circuit] = JsonConvert.DeserializeObject(PollCircuitResponse); + DigitalLoggerCircuit temp = CircuitStatus[circuit]; + Debug.Console(2, this, "DigitalLogger Circuit {0} Name: {1} State:{2}'", circuit, CircuitStatus[circuit].name, CircuitStatus[circuit].state); + CircuitNameFeedbacks[circuit].FireUpdate(); + CircuitState[circuit].FireUpdate(); + CircuitIsCritical[circuit].FireUpdate(); + } + catch (Exception e) + { + Debug.Console(0, this, "PollCircuit {0}", e); + + } + } + void Port_LineReceived(string response, HTTP_CALLBACK_ERROR error) + { + + + } + + public string SendRequest(string s) + { + HttpClientRequest request = new HttpClientRequest(); + string url = string.Format("http://{0}{1}", this.address, s); + request.Url = new UrlParser(url); + HttpClientResponse response = WebClient.Dispatch(request); + + Debug.Console(2, this, "DigitalLogger TX:\n'{0}'\nRX:\n'{1}'", url, response.ContentString); + return response.ContentString; + } + /// + /// Sends a command to the DSP (with delimiter appended) + /// + /// Command to send + /// + public void SendLine(string s) + { + + HttpClientRequest request = new HttpClientRequest(); + string url = string.Format("http://{0}{1}", this.address, s); + request.Url = new UrlParser(url); + + HttpClientResponse response = WebClient.Dispatch(request); + + Debug.Console(2, this, "DigitalLogger TX:\n'{0}'\nRX:\n'{1}'", url, response.ContentString); + + } + + public void CycleCircuit(uint circuit) + { + SendLine(String.Format("/outlet?{0}=CCL", circuit)); + //PollCircuit(circuit); + + } + + public void TurnOnCircuit(uint circuit) + { + SendLine(String.Format("/outlet?{0}=ON", circuit)); + //PollCircuit(circuit); + } + + public void TurnOffCircuit(uint circuit) + { + SendLine(String.Format("/outlet?{0}=Off", circuit)); + //PollCircuit(circuit); + } + + /// + /// Adds a command from a child module to the queue + /// + /// Command object from child module + public void EnqueueCommand(QueuedCommand commandToEnqueue) + { + CommandQueue.Enqueue(commandToEnqueue); + //Debug.Console(1, this, "Command (QueuedCommand) Enqueued '{0}'. CommandQueue has '{1}' Elements.", commandToEnqueue.Command, CommandQueue.Count); + + if(!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Adds a raw string command to the queue + /// + /// + public void EnqueueCommand(string command) + { + CommandQueue.Enqueue(command); + //Debug.Console(1, this, "Command (string) Enqueued '{0}'. CommandQueue has '{1}' Elements.", command, CommandQueue.Count); + + if (!CommandQueueInProgress) + SendNextQueuedCommand(); + } + + /// + /// Sends the next queued command to the DSP + /// + void SendNextQueuedCommand() + { + if (Communication.IsConnected && !CommandQueue.IsEmpty) + { + CommandQueueInProgress = true; + + if (CommandQueue.Peek() is QueuedCommand) + { + QueuedCommand nextCommand = new QueuedCommand(); + + nextCommand = (QueuedCommand)CommandQueue.Peek(); + + SendLine(nextCommand.Command); + } + else + { + string nextCommand = (string)CommandQueue.Peek(); + + SendLine(nextCommand); + } + } + + } + + + + public void CallPreset(ushort presetNumber) + { + SendLine(string.Format("Preset.Take = {0}", presetNumber)); + // SendLine("cgp 1"); + } + + public class QueuedCommand + { + public string Command { get; set; } + public string AttributeCode { get; set; } + // public QscDspControlPoint ControlPoint { get; set; } + } + + public class DigitalLoggerCircuit + { + public string name; + public bool locked; + public bool critical; + public bool transient_state; + public bool physical_state; + //public int cycle_delay; + public bool state; + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Properties/AssemblyInfo.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a7df216a --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; + +[assembly: AssemblyTitle("Essentials_Devices_Common")] +[assembly: AssemblyCompany("PepperDash Technology Corp")] +[assembly: AssemblyProduct("Essentials_Devices_Common")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyVersion("1.3.*")] \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Properties/ControlSystem.cfg b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Properties/ControlSystem.cfg new file mode 100644 index 00000000..e69de29b diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/SetTopBox/IRSetTopBoxBase.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/SetTopBox/IRSetTopBoxBase.cs new file mode 100644 index 00000000..4ec0264d --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/SetTopBox/IRSetTopBoxBase.cs @@ -0,0 +1,343 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Presets; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Common +{ + public class IRSetTopBoxBase : Device, ISetTopBoxControls, IUiDisplayInfo, IRoutingOutputs, IUsageTracking + { + public IrOutputPortController IrPort { get; private set; } + + public uint DisplayUiType { get { return DisplayUiConstants.TypeDirecTv; } } + + + public bool HasPresets { get; set; } + public bool HasDvr { get; set; } + public bool HasDpad { get; set; } + public bool HasNumeric { get; set; } + + public DevicePresetsModel PresetsModel { get; private set; } + + public IRSetTopBoxBase(string key, string name, IrOutputPortController portCont, + SetTopBoxPropertiesConfig props) + : base(key, name) + { + IrPort = portCont; + DeviceManager.AddDevice(portCont); + + HasPresets = props.HasPresets; + HasDvr = props.HasDvr; + HasDpad = props.HasDpad; + HasNumeric = props.HasNumeric; + + HasKeypadAccessoryButton1 = true; + KeypadAccessoryButton1Command = "Dash"; + KeypadAccessoryButton1Label = "-"; + + HasKeypadAccessoryButton2 = true; + KeypadAccessoryButton2Command = "NumericEnter"; + KeypadAccessoryButton2Label = "Enter"; + + AnyVideoOut = new RoutingOutputPort(RoutingPortNames.AnyVideoOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + AnyAudioOut = new RoutingOutputPort(RoutingPortNames.AnyAudioOut, eRoutingSignalType.Audio, + eRoutingPortConnectionType.DigitalAudio, null, this); + OutputPorts = new RoutingPortCollection { AnyVideoOut, AnyAudioOut }; + + } + + public void LoadPresets(string filePath) + { + PresetsModel = new DevicePresetsModel(Key + "-presets", this, filePath); + DeviceManager.AddDevice(PresetsModel); + } + + + #region ISetTopBoxControls Members + + public void DvrList(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_DVR, pressRelease); + } + + public void Replay(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_REPLAY, pressRelease); + } + + #endregion + + #region IDPad Members + + public void Up(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_UP_ARROW, pressRelease); + } + + public void Down(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_DN_ARROW, pressRelease); + } + + public void Left(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_LEFT_ARROW, pressRelease); + } + + public void Right(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_RIGHT_ARROW, pressRelease); + } + + public void Select(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_ENTER, pressRelease); + } + + public void Menu(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_MENU, pressRelease); + } + + public void Exit(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_EXIT, pressRelease); + } + + #endregion + + #region INumericKeypad Members + + public void Digit0(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_0, pressRelease); + } + + public void Digit1(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_1, pressRelease); + } + + public void Digit2(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_2, pressRelease); + } + + public void Digit3(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_3, pressRelease); + } + + public void Digit4(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_4, pressRelease); + } + + public void Digit5(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_5, pressRelease); + } + + public void Digit6(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_6, pressRelease); + } + + public void Digit7(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_7, pressRelease); + } + + public void Digit8(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_8, pressRelease); + } + + public void Digit9(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_9, pressRelease); + } + + /// + /// Defaults to true + /// + public bool HasKeypadAccessoryButton1 { get; set; } + + /// + /// Defaults to "-" + /// + public string KeypadAccessoryButton1Label { get; set; } + + + /// + /// Defaults to "Dash" + /// + public string KeypadAccessoryButton1Command { get; set; } + + public void KeypadAccessoryButton1(bool pressRelease) + { + IrPort.PressRelease(KeypadAccessoryButton1Command, pressRelease); + } + + /// + /// Defaults to true + /// + public bool HasKeypadAccessoryButton2 { get; set; } + + /// + /// Defaults to "Enter" + /// + public string KeypadAccessoryButton2Label { get; set; } + + + /// + /// Defaults to "Enter" + /// + public string KeypadAccessoryButton2Command { get; set; } + + public void KeypadAccessoryButton2(bool pressRelease) + { + IrPort.PressRelease(KeypadAccessoryButton2Command, pressRelease); + } + + #endregion + + #region ISetTopBoxNumericKeypad Members + + /// + /// Corresponds to "dash" IR command + /// + public void Dash(bool pressRelease) + { + IrPort.PressRelease("dash", pressRelease); + } + + /// + /// Corresponds to "numericEnter" IR command + /// + public void KeypadEnter(bool pressRelease) + { + IrPort.PressRelease("numericEnter", pressRelease); + } + + #endregion + + #region IChannelFunctions Members + + public void ChannelUp(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_CH_PLUS, pressRelease); + } + + public void ChannelDown(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_CH_MINUS, pressRelease); + } + + public void LastChannel(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_LAST, pressRelease); + } + + public void Guide(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_GUIDE, pressRelease); + } + + public void Info(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_INFO, pressRelease); + } + + #endregion + + #region IColorFunctions Members + + public void Red(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_RED, pressRelease); + } + + public void Green(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_GREEN, pressRelease); + } + + public void Yellow(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_YELLOW, pressRelease); + } + + public void Blue(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_BLUE, pressRelease); + } + + #endregion + + #region IRoutingOutputs Members + + public RoutingOutputPort AnyVideoOut { get; private set; } + public RoutingOutputPort AnyAudioOut { get; private set; } + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + #region ITransport Members + + public void ChapMinus(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_REPLAY, pressRelease); + } + + public void ChapPlus(bool pressRelease) + { + } + + public void FFwd(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_FSCAN, pressRelease); + } + + public void Pause(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_RSCAN, pressRelease); + } + + public void Play(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_PLAY, pressRelease); + } + + public void Record(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_RECORD, pressRelease); + } + + public void Rewind(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_RSCAN, pressRelease); + } + + public void Stop(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_STOP, pressRelease); + } + + #endregion + + #region IUsageTracking Members + + public UsageTracking UsageTracker { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/SetTopBox/SetTopBoxPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/SetTopBox/SetTopBoxPropertiesConfig.cs new file mode 100644 index 00000000..ae9a6709 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/SetTopBox/SetTopBoxPropertiesConfig.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Devices.Common +{ + public class SetTopBoxPropertiesConfig : PepperDash.Essentials.Core.Config.SourceDevicePropertiesConfigBase + { + public bool HasPresets { get; set; } + public bool HasDvr { get; set; } + public bool HasDpad { get; set; } + public bool HasNumeric { get; set; } + + public ControlPropertiesConfig Control { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Streaming/AppleTV.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Streaming/AppleTV.cs new file mode 100644 index 00000000..d8cd66b2 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Streaming/AppleTV.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Common +{ + public class AppleTV : Device, IDPad, ITransport, IUiDisplayInfo, IRoutingOutputs + { + + public IrOutputPortController IrPort { get; private set; } + public const string StandardDriverName = "Apple AppleTV-v2.ir"; + public uint DisplayUiType { get { return DisplayUiConstants.TypeAppleTv; } } + + public AppleTV(string key, string name, IrOutputPortController portCont) + : base(key, name) + { + IrPort = portCont; + DeviceManager.AddDevice(portCont); + + HdmiOut = new RoutingOutputPort(RoutingPortNames.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + AnyAudioOut = new RoutingOutputPort(RoutingPortNames.AnyAudioOut, eRoutingSignalType.Audio, + eRoutingPortConnectionType.DigitalAudio, null, this); + OutputPorts = new RoutingPortCollection { HdmiOut, AnyAudioOut }; + } + + + #region IDPad Members + + public void Up(bool pressRelease) + { + IrPort.PressRelease("+", pressRelease); + } + + public void Down(bool pressRelease) + { + IrPort.PressRelease("-", pressRelease); + } + + public void Left(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_TRACK_MINUS, pressRelease); + } + + public void Right(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_TRACK_PLUS, pressRelease); + } + + public void Select(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_ENTER, pressRelease); + } + + public void Menu(bool pressRelease) + { + IrPort.PressRelease("Menu", pressRelease); + } + + public void Exit(bool pressRelease) + { + + } + + #endregion + + #region ITransport Members + + public void Play(bool pressRelease) + { + IrPort.PressRelease("PLAY/PAUSE", pressRelease); + } + + public void Pause(bool pressRelease) + { + IrPort.PressRelease("PLAY/PAUSE", pressRelease); + } + + /// + /// Not implemented + /// + /// + public void Rewind(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void FFwd(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void ChapMinus(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void ChapPlus(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void Stop(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void Record(bool pressRelease) + { + } + + #endregion + + #region IRoutingOutputs Members + + public RoutingOutputPort HdmiOut { get; private set; } + public RoutingOutputPort AnyAudioOut { get; private set; } + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/Streaming/Roku.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Streaming/Roku.cs new file mode 100644 index 00000000..2d4af003 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/Streaming/Roku.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Common +{ + public class Roku2 : Device, IDPad, ITransport, IUiDisplayInfo, IRoutingOutputs + { + [Api] + public IrOutputPortController IrPort { get; private set; } + public const string StandardDriverName = "Roku XD_S.ir"; + [Api] + public uint DisplayUiType { get { return DisplayUiConstants.TypeRoku; } } + + public Roku2(string key, string name, IrOutputPortController portCont) + : base(key, name) + { + IrPort = portCont; + DeviceManager.AddDevice(portCont);; + + HdmiOut = new RoutingOutputPort(RoutingPortNames.HdmiOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + OutputPorts = new RoutingPortCollection { HdmiOut }; + } + + #region IDPad Members + + [Api] + public void Up(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_UP_ARROW, pressRelease); + } + + [Api] + public void Down(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_DN_ARROW, pressRelease); + } + + [Api] + public void Left(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_LEFT_ARROW, pressRelease); + } + + [Api] + public void Right(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_RIGHT_ARROW, pressRelease); + } + + [Api] + public void Select(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_ENTER, pressRelease); + } + + [Api] + public void Menu(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_MENU, pressRelease); + } + + [Api] + public void Exit(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_EXIT, pressRelease); + } + + #endregion + + #region ITransport Members + + [Api] + public void Play(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_PLAY, pressRelease); + } + + [Api] + public void Pause(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_PAUSE, pressRelease); + } + + [Api] + public void Rewind(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_RSCAN, pressRelease); + } + + [Api] + public void FFwd(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_FSCAN, pressRelease); + } + + /// + /// Not implemented + /// + /// + public void ChapMinus(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void ChapPlus(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void Stop(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void Record(bool pressRelease) + { + } + + #endregion + + #region IRoutingOutputs Members + + public RoutingOutputPort HdmiOut { get; private set; } + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/BookingsDataClasses.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/BookingsDataClasses.cs new file mode 100644 index 00000000..46d171cf --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/BookingsDataClasses.cs @@ -0,0 +1,379 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + public class CiscoCodecBookings + { + public class TotalRows + { + public string Value { get; set; } + } + + public class ResultInfo + { + public TotalRows TotalRows { get; set; } + } + + public class LastUpdated + { + string _value; + + public DateTime Value { + get + { + DateTime _valueDateTime; + try + { + _valueDateTime = DateTime.Parse(_value); + return _valueDateTime; + } + catch + { + return new DateTime(); + } + } + set + { + _value = value.ToString(); + } + } + } + + public class Id + { + public string Value { get; set; } + } + + public class Title + { + public string Value { get; set; } + } + + public class Agenda + { + public string Value { get; set; } + } + + public class Privacy + { + public string Value { get; set; } + } + + public class FirstName + { + public string Value { get; set; } + } + + public class LastName + { + public string Value { get; set; } + } + + public class Email + { + public string Value { get; set; } + } + + public class Id2 + { + public string Value { get; set; } + } + + public class Organizer + { + public FirstName FirstName { get; set; } + public LastName LastName { get; set; } + public Email Email { get; set; } + public Id2 Id { get; set; } + + public Organizer() + { + FirstName = new FirstName(); + LastName = new LastName(); + Email = new Email(); + } + } + + public class StartTime + { + public DateTime Value { get; set; } + } + + public class StartTimeBuffer + { + public string Value { get; set; } + } + + public class EndTime + { + public DateTime Value { get; set; } + } + + public class EndTimeBuffer + { + public string Value { get; set; } + } + + public class Time + { + public StartTime StartTime { get; set; } + public StartTimeBuffer StartTimeBuffer { get; set; } + public EndTime EndTime { get; set; } + public EndTimeBuffer EndTimeBuffer { get; set; } + + public Time() + { + StartTime = new StartTime(); + EndTime = new EndTime(); + } + } + + public class MaximumMeetingExtension + { + public string Value { get; set; } + } + + public class MeetingExtensionAvailability + { + public string Value { get; set; } + } + + public class BookingStatus + { + public string Value { get; set; } + } + + public class BookingStatusMessage + { + public string Value { get; set; } + } + + public class Enabled + { + public string Value { get; set; } + } + + public class Url + { + public string Value { get; set; } + } + + public class MeetingNumber + { + public string Value { get; set; } + } + + public class Password + { + public string Value { get; set; } + } + + public class HostKey + { + public string Value { get; set; } + } + + public class DialInNumbers + { + } + + public class Webex + { + public Enabled Enabled { get; set; } + public Url Url { get; set; } + public MeetingNumber MeetingNumber { get; set; } + public Password Password { get; set; } + public HostKey HostKey { get; set; } + public DialInNumbers DialInNumbers { get; set; } + } + + public class Encryption + { + public string Value { get; set; } + } + + public class Role + { + public string Value { get; set; } + } + + public class Recording + { + public string Value { get; set; } + } + + public class Number + { + public string Value { get; set; } + } + + public class Protocol + { + public string Value { get; set; } + } + + public class CallRate + { + public string Value { get; set; } + } + + public class CallType + { + public string Value { get; set; } + } + + public class Call + { + public string id { get; set; } + public Number Number { get; set; } + public Protocol Protocol { get; set; } + public CallRate CallRate { get; set; } + public CallType CallType { get; set; } + } + + public class Calls + { + public List Call {get; set;} + } + + public class ConnectMode + { + public string Value { get; set; } + } + + public class DialInfo + { + public Calls Calls { get; set; } + public ConnectMode ConnectMode { get; set; } + + public DialInfo() + { + Calls = new Calls(); + ConnectMode = new ConnectMode(); + } + } + + public class Booking + { + public string id { get; set; } + public Id Id { get; set; } + public Title Title { get; set; } + public Agenda Agenda { get; set; } + public Privacy Privacy { get; set; } + public Organizer Organizer { get; set; } + public Time Time { get; set; } + public MaximumMeetingExtension MaximumMeetingExtension { get; set; } + public MeetingExtensionAvailability MeetingExtensionAvailability { get; set; } + public BookingStatus BookingStatus { get; set; } + public BookingStatusMessage BookingStatusMessage { get; set; } + public Webex Webex { get; set; } + public Encryption Encryption { get; set; } + public Role Role { get; set; } + public Recording Recording { get; set; } + public DialInfo DialInfo { get; set; } + + public Booking() + { + Time = new Time(); + Id = new Id(); + Organizer = new Organizer(); + Title = new Title(); + Agenda = new Agenda(); + Privacy = new Privacy(); + DialInfo = new DialInfo(); + } + } + + public class BookingsListResult + { + public string status { get; set; } + public ResultInfo ResultInfo { get; set; } + //public LastUpdated LastUpdated { get; set; } + public List Booking { get; set; } + } + + public class CommandResponse + { + public BookingsListResult BookingsListResult { get; set; } + } + + public class RootObject + { + public CommandResponse CommandResponse { get; set; } + } + + /// + /// Extracts the necessary meeting values from the Cisco bookings response ans converts them to the generic class + /// + /// + /// + public static List GetGenericMeetingsFromBookingResult(List bookings) + { + var meetings = new List(); + + if (Debug.Level > 0) + { + Debug.Console(1, "Meetings List:\n"); + } + + foreach(Booking b in bookings) + { + var meeting = new Meeting(); + + if(b.Id != null) + meeting.Id = b.Id.Value; + if(b.Organizer != null) + meeting.Organizer = string.Format("{0}, {1}", b.Organizer.LastName.Value, b.Organizer.FirstName.Value); + if(b.Title != null) + meeting.Title = b.Title.Value; + if(b.Agenda != null) + meeting.Agenda = b.Agenda.Value; + if(b.Time != null) + meeting.StartTime = b.Time.StartTime.Value; + meeting.EndTime = b.Time.EndTime.Value; + if(b.Privacy != null) + meeting.Privacy = CodecCallPrivacy.ConvertToDirectionEnum(b.Privacy.Value); + +//#warning Update this ConnectMode conversion after testing onsite. Expected value is "OBTP", but in PD NYC Test scenarios, "Manual" is being returned for OBTP meetings + if (b.DialInfo.ConnectMode != null) + if (b.DialInfo.ConnectMode.Value.ToLower() == "obtp" || b.DialInfo.ConnectMode.Value.ToLower() == "manual") + meeting.IsOneButtonToPushMeeting = true; + + if (b.DialInfo.Calls.Call != null) + { + foreach (Call c in b.DialInfo.Calls.Call) + { + meeting.Calls.Add(new PepperDash.Essentials.Devices.Common.Codec.Call() + { + Number = c.Number.Value, + Protocol = c.Protocol.Value, + CallRate = c.CallRate.Value, + CallType = c.CallType.Value + }); + } + } + + + meetings.Add(meeting); + + if(Debug.Level > 0) + { + Debug.Console(1, "Title: {0}, ID: {1}, Organizer: {2}, Agenda: {3}", meeting.Title, meeting.Id, meeting.Organizer, meeting.Agenda); + Debug.Console(1, " Start Time: {0}, End Time: {1}, Duration: {2}", meeting.StartTime, meeting.EndTime, meeting.Duration); + Debug.Console(1, " Joinable: {0}\n", meeting.Joinable); + } + } + + meetings.OrderBy(m => m.StartTime); + + return meetings; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CallHistoryDataClasses.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CallHistoryDataClasses.cs new file mode 100644 index 00000000..4fc07bd2 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CallHistoryDataClasses.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + public class CiscoCallHistory + { + public class CallbackNumber + { + public string Value { get; set; } + } + + public class DisplayName + { + public string Value { get; set; } + } + + public class LastOccurrenceStartTime + { + public DateTime Value { get; set; } + } + + public class LastOccurrenceDaysAgo + { + public string Value { get; set; } + } + + public class LastOccurrenceHistoryId + { + public string Value { get; set; } + } + + public class OccurrenceType + { + public string Value { get; set; } + } + + public class IsAcknowledged + { + public string Value { get; set; } + } + + public class OccurrenceCount + { + public string Value { get; set; } + } + + public class Entry + { + public string id { get; set; } + public CallbackNumber CallbackNumber { get; set; } + public DisplayName DisplayName { get; set; } + public LastOccurrenceStartTime LastOccurrenceStartTime { get; set; } + public LastOccurrenceDaysAgo LastOccurrenceDaysAgo { get; set; } + public LastOccurrenceHistoryId LastOccurrenceHistoryId { get; set; } + public OccurrenceType OccurrenceType { get; set; } + public IsAcknowledged IsAcknowledged { get; set; } + public OccurrenceCount OccurrenceCount { get; set; } + } + + public class Offset + { + public string Value { get; set; } + } + + public class Limit + { + public string Value { get; set; } + } + + public class ResultInfo + { + public Offset Offset { get; set; } + public Limit Limit { get; set; } + } + + public class CallHistoryRecentsResult + { + public string status { get; set; } + public List Entry { get; set; } + public ResultInfo ResultInfo { get; set; } + } + + public class CommandResponse + { + public CallHistoryRecentsResult CallHistoryRecentsResult { get; set; } + } + + public class RootObject + { + public CommandResponse CommandResponse { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoCamera.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoCamera.cs new file mode 100644 index 00000000..12819336 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoCamera.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Devices.Common.Cameras; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco +{ + public class CiscoFarEndCamera : CameraBase, IHasCameraPtzControl, IAmFarEndCamera + { + protected CiscoSparkCodec ParentCodec { get; private set; } + + protected string CallId { + get + { + return (ParentCodec as CiscoSparkCodec).GetCallId(); + } + } + + public CiscoFarEndCamera(string key, string name, CiscoSparkCodec codec) + : base(key, name) + { + Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom; + + ParentCodec = codec; + } + + #region IHasCameraPtzControl Members + + public void PositionHome() + { + // Not supported on far end camera + } + + #endregion + + #region IHasCameraPanControl Members + + public void PanLeft() + { + ParentCodec.SendText(string.Format("xCommand Call FarEndControl Camera Move Value: Left CallId: {0}", CallId)); + } + + public void PanRight() + { + ParentCodec.SendText(string.Format("xCommand Call FarEndControl Camera Move Value: Right CallId: {0}", CallId)); + } + + public void PanStop() + { + Stop(); + } + + #endregion + + #region IHasCameraTiltControl Members + + public void TiltDown() + { + ParentCodec.SendText(string.Format("xCommand Call FarEndControl Camera Move Value: Down CallId: {0}", CallId)); + } + + public void TiltUp() + { + ParentCodec.SendText(string.Format("xCommand Call FarEndControl Camera Move Value: Up CallId: {0}", CallId)); + } + + public void TiltStop() + { + Stop(); + } + + #endregion + + #region IHasCameraZoomControl Members + + public void ZoomIn() + { + ParentCodec.SendText(string.Format("xCommand Call FarEndControl Camera Move Value: ZoomIn CallId: {0}", CallId)); + } + + public void ZoomOut() + { + ParentCodec.SendText(string.Format("xCommand Call FarEndControl Camera Move Value: ZoomOut CallId: {0}", CallId)); + } + + public void ZoomStop() + { + Stop(); + } + + #endregion + + + void Stop() + { + ParentCodec.SendText(string.Format("xCommand Call FarEndControl Camera Stop CallId: {0}", CallId)); + } + } + + public class CiscoSparkCamera : CameraBase, IHasCameraPtzControl, IHasCameraFocusControl + { + /// + /// The codec this camera belongs to + /// + protected CiscoSparkCodec ParentCodec { get; private set; } + + /// + /// The ID of the camera on the codec + /// + protected uint CameraId { get; private set; } + + /// + /// Valid range 1-15 + /// + protected uint PanSpeed { get; private set; } + + /// + /// Valid range 1-15 + /// + protected uint TiltSpeed { get; private set; } + + /// + /// Valid range 1-15 + /// + protected uint ZoomSpeed { get; private set; } + + private bool isPanning; + + private bool isTilting; + + private bool isZooming; + + private bool isFocusing; + + private bool isMoving + { + get + { + return isPanning || isTilting || isZooming || isFocusing; + + } + } + + public CiscoSparkCamera(string key, string name, CiscoSparkCodec codec, uint id) + : base(key, name) + { + // Default to all capabilties + Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom | eCameraCapabilities.Focus; + + ParentCodec = codec; + + CameraId = id; + + // Set default speeds + PanSpeed = 7; + TiltSpeed = 7; + ZoomSpeed = 7; + } + + + // Takes a string from the camera capabilities value and converts from "ptzf" to enum bitmask + public void SetCapabilites(string capabilites) + { + var c = capabilites.ToLower(); + + if (c.Contains("p")) + Capabilities = Capabilities | eCameraCapabilities.Pan; + + if (c.Contains("t")) + Capabilities = Capabilities | eCameraCapabilities.Tilt; + + if (c.Contains("z")) + Capabilities = Capabilities | eCameraCapabilities.Zoom; + + if (c.Contains("f")) + Capabilities = Capabilities | eCameraCapabilities.Focus; + } + + #region IHasCameraPtzControl Members + + public void PositionHome() + { + // Not supported on Internal Spark Camera + + + } + + #endregion + + #region IHasCameraPanControl Members + + public void PanLeft() + { + if (!isMoving) + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Pan: Left PanSpeed: {1}", CameraId, PanSpeed)); + isPanning = true; + } + } + + public void PanRight() + { + if (!isMoving) + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Pan: Right PanSpeed: {1}", CameraId, PanSpeed)); + isPanning = true; + } + } + + public void PanStop() + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Pan: Stop", CameraId)); + isPanning = false; + } + + #endregion + + + + #region IHasCameraTiltControl Members + + public void TiltDown() + { + if (!isMoving) + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Tilt: Down TiltSpeed: {1}", CameraId, TiltSpeed)); + isTilting = true; + } + } + + public void TiltUp() + { + if (!isMoving) + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Tilt: Up TiltSpeed: {1}", CameraId, TiltSpeed)); + isTilting = true; + } + } + + public void TiltStop() + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Tilt: Stop", CameraId)); + isTilting = false; + } + + #endregion + + #region IHasCameraZoomControl Members + + public void ZoomIn() + { + if (!isMoving) + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Zoom: In ZoomSpeed: {1}", CameraId, ZoomSpeed)); + isZooming = true; + } + } + + public void ZoomOut() + { + if (!isMoving) + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Zoom: Out ZoomSpeed: {1}", CameraId, ZoomSpeed)); + isZooming = true; + } + } + + public void ZoomStop() + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Zoom: Stop", CameraId)); + isZooming = false; + } + + #endregion + + #region IHasCameraFocusControl Members + + public void FocusNear() + { + if (!isMoving) + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Focus: Near", CameraId)); + isFocusing = true; + } + } + + public void FocusFar() + { + if (!isMoving) + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Focus: Far", CameraId)); + isFocusing = true; + } + } + + public void FocusStop() + { + ParentCodec.SendText(string.Format("xCommand Camera Ramp CameraId: {0} Focus: Stop", CameraId)); + isFocusing = false; + } + + public void TriggerAutoFocus() + { + ParentCodec.SendText(string.Format("xCommand Camera TriggerAutofocus CameraId: {0}", CameraId)); + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoCodec.cs.orig b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoCodec.cs.orig new file mode 100644 index 00000000..3abc8d01 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoCodec.cs.orig @@ -0,0 +1,1359 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.Net.Https; +using Crestron.SimplSharp.CrestronXml; +using Crestron.SimplSharp.CrestronXml.Serialization; +using Newtonsoft.Json; +<<<<<<< HEAD +//using Cisco_One_Button_To_Push; +//using Cisco_SX80_Corporate_Phone_Book; +======= +>>>>>>> origin/feature/ecs-342 + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.Occupancy; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco +{ + enum eCommandType { SessionStart, SessionEnd, Command, GetStatus, GetConfiguration }; + + public class CiscoCodec : VideoCodecBase, IHasCallHistory, IHasCallFavorites, IHasDirectory, IHasScheduleAwareness, IOccupancyStatusProvider, IHasCodecLayouts, IHasCodecSelfview + { + public event EventHandler DirectoryResultReturned; + + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + public CommunicationGather JsonGather { get; private set; } + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + public BoolFeedback StandbyIsOnFeedback { get; private set; } + + public BoolFeedback RoomIsOccupiedFeedback { get; private set; } + + public IntFeedback PeopleCountFeedback { get; private set; } + + public BoolFeedback SpeakerTrackIsOnFeedback { get; private set; } + + public BoolFeedback SelfviewIsOnFeedback { get; private set; } + + public StringFeedback SelfviewPipPositionFeedback { get; private set; } + + public StringFeedback LocalLayoutFeedback { 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; + + private CiscoCodecStatus.RootObject CodecStatus; + + public CodecCallHistory CallHistory { get; private set; } + + public CodecCallFavorites CallFavorites { get; private set; } + + public CodecDirectory DirectoryRoot { 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 Func StandbyStateFeedbackFunc + { + get + { + return () => CodecStatus.Status.Standby.State.BoolValue; + } + } + + /// + /// Gets the value of the currently shared source, or returns null + /// + protected override Func SharingSourceFeedbackFunc + { +#warning verify that source feedback to room works from codec + get + { + return () => PresentationSourceKey; + } + } + + 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; + } + } + + private string CliFeedbackRegistrationExpression; + + private CodecSyncState SyncState; + + private CodecPhonebookSyncState PhonebookSyncState; + + private StringBuilder JsonMessage; + + private bool JsonFeedbackMessageIsIncoming; + + public bool CommDebuggingIsOn; + + string Delimiter = "\r\n"; + + int PresentationSource; + + string PresentationSourceKey; + + string PhonebookMode = "Local"; // Default to Local + + int PhonebookResultsLimit = 255; // Could be set later by config. + + CTimer LoginMessageReceived; + + public RoutingInputPort CodecOsdIn { get; private set; } + public RoutingInputPort HdmiIn1 { get; private set; } + public RoutingInputPort HdmiIn2 { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + // Constructor for IBasicCommunication + public CiscoCodec(string key, string name, IBasicCommunication comm, CiscoCodecPropertiesConfig props ) + : base(key, name) + { + StandbyIsOnFeedback = new BoolFeedback(StandbyStateFeedbackFunc); + RoomIsOccupiedFeedback = new BoolFeedback(RoomIsOccupiedFeedbackFunc); + PeopleCountFeedback = new IntFeedback(PeopleCountFeedbackFunc); + SpeakerTrackIsOnFeedback = new BoolFeedback(SpeakerTrackIsOnFeedbackFunc); + SelfviewIsOnFeedback = new BoolFeedback(SelfViewIsOnFeedbackFunc); + SelfviewPipPositionFeedback = new StringFeedback(SelfviewPipPositionFeedbackFunc); + LocalLayoutFeedback = new StringFeedback(LocalLayoutFeedbackFunc); + + Communication = comm; + + LoginMessageReceived = new CTimer(DisconnectClientAndReconnect, 5000); + + 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"); + } + + 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; + + //CodecObtp = new CiscoOneButtonToPush(); + + //PhoneBook = new Corporate_Phone_Book(); + + CodecConfiguration = new CiscoCodecConfiguration.RootObject(); + CodecStatus = new CiscoCodecStatus.RootObject(); + + CodecInfo = new CiscoCodecInfo(CodecStatus, CodecConfiguration); + + CallHistory = new CodecCallHistory(); + + if (props.Favorites != null) + { + CallFavorites = new CodecCallFavorites(); + CallFavorites.Favorites = props.Favorites; + } + + DirectoryRoot = new CodecDirectory(); + + CodecSchedule = new CodecScheduleAwareness(); + + //Set Feedback Actions + 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 = SpeakerTrackIsOnFeedback.FireUpdate; + CodecStatus.Status.Video.Selfview.Mode.ValueChangedAction = SelfviewIsOnFeedback.FireUpdate; + CodecStatus.Status.Video.Selfview.PIPPosition.ValueChangedAction = ComputeSelfviewPipStatus; + CodecStatus.Status.Video.Layout.LayoutFamily.Local.ValueChangedAction = ComputeLocalLayout; + } + + /// + /// Starts the HTTP feedback server and syncronizes state of codec + /// + /// + public override bool CustomActivate() + { + CrestronConsole.AddNewConsoleCommand(SendText, "send" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + 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); + + //CommDebuggingIsOn = true; + Communication.Connect(); + var socket = Communication as ISocketStatus; + if (socket != null) + { + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + + CommunicationMonitor.Start(); + + CodecOsdIn = new RoutingInputPort(RoutingPortNames.CodecOsd, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, new Action(StopSharing), this); + HdmiIn1 = new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, new Action(SelectPresentationSource1), this); + HdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, new Action(SelectPresentationSource2), this); + + HdmiOut = new RoutingOutputPort(RoutingPortNames.HdmiOut, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this); + + InputPorts.Add(CodecOsdIn); + InputPorts.Add(HdmiIn1); + InputPorts.Add(HdmiIn2); + OutputPorts.Add(HdmiOut); + + + 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/Standby" + Delimiter + + prefix + "/Status/Video/Selfview" + Delimiter + + prefix + "/Bookings" + Delimiter + + prefix + "/Event/CallDisconnect" + Delimiter; + + return base.CustomActivate(); + } + + /// + /// 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(); + + GetPhonebook(null); + + 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) + { + if (e.Client.IsConnected) + { + //LoginMessageReceived.Reset(); + } + else + { + SyncState.CodecDisconnected(); + PhonebookSyncState.CodecDisconnected(); + } + } + + void DisconnectClientAndReconnect(object o) + { + Debug.Console(0, this, "Disconnecting and Reconnecting to codec."); + + Communication.Disconnect(); + + 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()); + + // Forward the complete message to be deserialized + DeserializeResponse(JsonMessage.ToString()); + 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": + { + LoginMessageReceived.Stop(); + SendText("xPreferences outputmode json"); + break; + } + case "xpreferences outputmode json": + { + if (!SyncState.InitialStatusMessageWasReceived) + SendText("xStatus"); + break; + } + case "xfeedback register /event/calldisconnect": + { + SyncState.FeedbackRegistered(); + break; + } + } + } + + } + + 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 mising members + JsonSerializerSettings settings = new JsonSerializerSettings(); + settings.NullValueHandling = NullValueHandling.Ignore; + settings.MissingMemberHandling = MissingMemberHandling.Ignore; + settings.ObjectCreationHandling = ObjectCreationHandling.Auto; + + if (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 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; + + // Update properties of ActiveCallItem + if(call.Status != null) + if (!string.IsNullOrEmpty(call.Status.Value)) + { + eCodecCallStatus newStatus = eCodecCallStatus.Unknown; + + newStatus = CodecCallStatus.ConvertToStatusEnum(call.Status.Value); + + if (newStatus != eCodecCallStatus.Unknown) + { + changeDetected = true; + SetNewCallStatusAndFireCallStatusChange(newStatus, tempActiveCall); + } + + if (newStatus == eCodecCallStatus.Connected) + GetCallHistory(); + } + 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) + { + 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(); + + SetNewCallStatusAndFireCallStatusChange(newCallItem.Status, newCallItem); + } + + } + + } + + JsonConvert.PopulateObject(response, CodecStatus); + + if (!SyncState.InitialStatusMessageWasReceived) + { + SyncState.InitialStatusMessageReceived(); + + if (!SyncState.InitialConfigurationMessageWasReceived) + SendText("xConfiguration"); + } + } + else if (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) + { + // Event Message + + CiscoCodecEvents.RootObject eventReceived = new CiscoCodecEvents.RootObject(); + + JsonConvert.PopulateObject(response, eventReceived); + + EvalutateEvent(eventReceived); + } + else if (response.IndexOf("\"CommandResponse\":{") > -1) + { + // CommandResponse Message + + if (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) + { + GetCallHistory(); + } + else if (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(); + + PrintPhonebook(DirectoryRoot); + } + else if (PhonebookSyncState.InitialSyncComplete) + { + var directoryResults = new CodecDirectory(); + + directoryResults = CiscoCodecPhonebook.ConvertCiscoPhonebookToGeneric(codecPhonebookResponse.CommandResponse.PhonebookSearchResult); + + PrintPhonebook(directoryResults); + + // 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 = directoryResults }); + + // Fire some sort of callback delegate to the UI that requested the directory search results + } + } + else if (response.IndexOf("\"BookingsListResult\":{") > -1) + { + var codecBookings = new CiscoCodecBookings.RootObject(); + + JsonConvert.PopulateObject(response, codecBookings); + + CodecSchedule.Meetings = CiscoCodecBookings.GetGenericMeetingsFromBookingResult(codecBookings.CommandResponse.BookingsListResult.Booking); + + + } + + } + + } + catch (Exception ex) + { + Debug.Console(1, this, "Error Deserializing feedback from codec: {0}", ex); + } + } + + /// + /// Evaluates an event received from the codec + /// + /// + void EvalutateEvent(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(); + + // Notify of the call disconnection + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, tempActiveCall); + + GetCallHistory(); + } + } + } + + public override void ExecuteSwitch(object selector) + { + (selector as Action)(); + PresentationSourceKey = selector.ToString(); + } + + protected override Func IncomingCallFeedbackFunc { get { return () => false; } } + + /// + /// Gets the first CallId or returns null + /// + /// + private string GetCallId() + { + string callId = null; + + if (ActiveCalls.Count > 1) + { + foreach (CodecActiveCallItem call in ActiveCalls) ; + } + else if (ActiveCalls.Count == 1) + callId = ActiveCalls[0].Id; + + return callId; + + } + + private void GetCallHistory() + { + SendText("xCommand CallHistory Recents Limit: 20 Order: OccurrenceTime"); + } + + /// + /// Gets the bookings for today + /// + /// + public void GetBookings(string command) + { + SendText("xCommand Bookings List Days: 1 DayOffset: 0"); + } + + /// + /// 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", PhonebookMode)); + } + + /// + /// 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: Contact Limit: {2}", folderId, PhonebookMode, PhonebookResultsLimit)); + } + + void PrintPhonebook(CodecDirectory directory) + { + if (Debug.Level > 0) + { + Debug.Console(1, this, "Directory Results:\n"); + + foreach (DirectoryItem item in directory.DirectoryResults) + { + if (item is DirectoryFolder) + { + Debug.Console(1, this, "+ {0}", item.Name); + } + else if (item is DirectoryContact) + { + Debug.Console(1, this, "{0}", item.Name); + } + } + } + } + + public override void Dial(string s) + { + SendText(string.Format("xCommand Dial Number: \"{0}\"", s)); + } + + public void DialBookingId(string s) + { + SendText(string.Format("xCommand Dial BookingId: {0}", s)); + } + + 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) + { + if (CallFavorites != null) + { + 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(1); + } + + /// + /// Select source 2 as the presetnation source + /// + public void SelectPresentationSource2() + { + SelectPresentationSource(2); + } + + public override void StartSharing() + { + string sendingMode = string.Empty; + + if (IsInCall) + sendingMode = "LocalRemote"; + else + sendingMode = "LocalOnly"; + + SendText(string.Format("xCommand Presentation Start PresentationSource: {0}", PresentationSource)); + } + + public override void StopSharing() + { + SendText(string.Format("xCommand Presentation Stop PresentationSource: {0}", PresentationSource)); + } + + 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 void StandbyActivate() + { + SendText("xCommand Standby Activate"); + } + + /// + /// Wakes the codec from standby + /// + public void StandbyDeactivate() + { + SendText("xCommand Standby Deactivate"); + } + + /// + /// Reboots the codec + /// + public void Reboot() + { + SendText("xCommand SystemUnit Boot Action: Restart"); + } + + /// + /// 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]); + } + } + + /// + /// 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)); + } + + public class CiscoCodecInfo : VideoCodecInfo + { + public CiscoCodecStatus.RootObject CodecStatus { get; private set; } + + public CiscoCodecConfiguration.RootObject CodecConfiguration { get; private set; } + + public override bool MultiSiteOptionIsEnabled + { + get + { + if (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 PhoneNumber + { + get + { + if (CodecConfiguration.Configuration.H323.H323Alias.E164 != null) + return CodecConfiguration.Configuration.H323.H323Alias.E164.Value; + else + return string.Empty; + } + } + public override string SipUri + { + get + { + if (CodecConfiguration.Configuration.H323.H323Alias.ID != null) + return CodecConfiguration.Configuration.H323.H323Alias.ID.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; + } + } + } + + /// + /// 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 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 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() + { + InitialConfigurationMessageWasReceived = false; + InitialStatusMessageWasReceived = false; + FeedbackWasRegistered = false; + InitialSyncComplete = false; + } + + void CheckSyncStatus() + { + if (InitialConfigurationMessageWasReceived && InitialStatusMessageWasReceived && FeedbackWasRegistered) + { + InitialSyncComplete = true; + Debug.Console(1, this, "Initial Codec Sync Complete!"); + } + else + InitialSyncComplete = false; + } + } + + /// + /// Used to track the status of syncronizing the phonebook values when connecting to a codec or refreshing the phonebook info + /// + public class CodecPhonebookSyncState : 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 InitialPhonebookFoldersWasReceived { get; private set; } + + public bool NumberOfContactsWasReceived { get; private set; } + + public bool PhonebookRootEntriesWasRecieved { get; private set; } + + public bool PhonebookHasFolders { get; private set; } + + public int NumberOfContacts { get; private set; } + + public CodecPhonebookSyncState(string key) + { + Key = key; + + CodecDisconnected(); + } + + public void InitialPhonebookFoldersReceived() + { + InitialPhonebookFoldersWasReceived = true; + + CheckSyncStatus(); + } + + public void PhonebookRootEntriesReceived() + { + PhonebookRootEntriesWasRecieved = true; + + CheckSyncStatus(); + } + + public void SetPhonebookHasFolders(bool value) + { + PhonebookHasFolders = value; + + Debug.Console(1, this, "Phonebook has folders: {0}", PhonebookHasFolders); + } + + public void SetNumberOfContacts(int contacts) + { + NumberOfContacts = contacts; + NumberOfContactsWasReceived = true; + + Debug.Console(1, this, "Phonebook contains {0} contacts.", NumberOfContacts); + + CheckSyncStatus(); + } + + public void CodecDisconnected() + { + InitialPhonebookFoldersWasReceived = false; + PhonebookHasFolders = false; + NumberOfContacts = 0; + NumberOfContactsWasReceived = false; + } + + void CheckSyncStatus() + { + if (InitialPhonebookFoldersWasReceived && NumberOfContactsWasReceived && PhonebookRootEntriesWasRecieved) + { + InitialSyncComplete = true; + Debug.Console(1, this, "Initial Phonebook Sync Complete!"); + } + else + InitialSyncComplete = false; + } + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..38344f18 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodec.cs @@ -0,0 +1,1846 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.Occupancy; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco +{ + enum eCommandType { SessionStart, SessionEnd, Command, GetStatus, GetConfiguration }; + + public class CiscoSparkCodec : VideoCodecBase, IHasCallHistory, IHasCallFavorites, IHasDirectory, + IHasScheduleAwareness, IOccupancyStatusProvider, IHasCodecLayouts, IHasCodecSelfView, + ICommunicationMonitor, IRouting, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets + { + public event EventHandler DirectoryResultReturned; + + public CommunicationGather PortGather { get; private set; } + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + 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 + + int 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()); + + 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); + + 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 + 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.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; + CodecStatus.Status.Conference.Presentation.Mode.ValueChangedAction += FarEndIsSharingContentFeedback.FireUpdate; + + CodecOsdIn = new RoutingInputPort(RoutingPortNames.CodecOsd, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(StopSharing), this); + HdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(SelectPresentationSource1), this); + HdmiIn3 = new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(SelectPresentationSource2), this); + + HdmiOut1 = new RoutingOutputPort(RoutingPortNames.HdmiOut1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + HdmiOut2 = new RoutingOutputPort(RoutingPortNames.HdmiOut2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + InputPorts.Add(CodecOsdIn); + InputPorts.Add(HdmiIn2); + InputPorts.Add(HdmiIn3); + OutputPorts.Add(HdmiOut1); + + SetUpCameras(); + + CreateOsdSource(); + } + + + /// + /// 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); + } + + /// + /// 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); + + var socket = Communication as ISocketStatus; + if (socket != null) + { + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + + Communication.Connect(); + + CommunicationMonitor.Start(); + + 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 + "/Bookings" + Delimiter + + prefix + "/Event/CallDisconnect" + Delimiter + + prefix + "/Event/Bookings" + Delimiter + + prefix + "/Event/CameraPresetListUpdated" + Delimiter; + + return base.CustomActivate(); + } + + /// + /// 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()); + + // Forward the complete message to be deserialized + DeserializeResponse(JsonMessage.ToString()); + 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; + } + } + } + + } + + 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) + { + // 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) + { + // Configuration Message + + JsonConvert.PopulateObject(response, CodecConfiguration); + + if (!SyncState.InitialConfigurationMessageWasReceived) + { + SyncState.InitialConfigurationMessageReceived(); + if (!SyncState.FeedbackWasRegistered) + { + SendText(CliFeedbackRegistrationExpression); + } + } + + } + else if (response.IndexOf("\"Event\":{") > -1) + { + if (response.IndexOf("\"CallDisconnect\":{") > -1) + { + CiscoCodecEvents.RootObject eventReceived = new CiscoCodecEvents.RootObject(); + + JsonConvert.PopulateObject(response, eventReceived); + + EvalutateDisconnectEvent(eventReceived); + } + else if (response.IndexOf("\"Bookings\":{") > -1) // The list has changed, reload it + { + GetBookings(null); + } + } + else if (response.IndexOf("\"CommandResponse\":{") > -1) + { + // CommandResponse Message + + if (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) + { + GetCallHistory(); + } + else if (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) + { + if (CallFavorites != null) + { + 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"); + } + + /// + /// 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() + { + SendText("xCommand Cameras SpeakerTrack Activate"); + } + + public void CameraAutoModeOff() + { + 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(); + + 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.H323Alias.E164 != null) + return CodecConfiguration.Configuration.H323.H323Alias.E164.Value; + else + return string.Empty; + } + } + public override string H323Id + { + get + { + if (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.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.AlternateURI.Primary.URI.Value != null) + return CodecStatus.Status.SIP.AlternateURI.Primary.URI.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)); + } + + } + + /// + /// 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; + } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodec.cs.orig b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodec.cs.orig new file mode 100644 index 00000000..9ce58609 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodec.cs.orig @@ -0,0 +1,1487 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharp.Net.Https; +using Crestron.SimplSharp.CrestronXml; +using Crestron.SimplSharp.CrestronXml.Serialization; +using Newtonsoft.Json; +//using Cisco_One_Button_To_Push; +//using Cisco_SX80_Corporate_Phone_Book; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.Occupancy; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco +{ + enum eCommandType { SessionStart, SessionEnd, Command, GetStatus, GetConfiguration }; + + public class CiscoSparkCodec : VideoCodecBase, IHasCallHistory, IHasCallFavorites, IHasDirectory, + IHasScheduleAwareness, IOccupancyStatusProvider, IHasCodecLayouts, IHasCodecSelfview, + ICommunicationMonitor, IRouting + { + public event EventHandler DirectoryResultReturned; + + public CommunicationGather PortGather { get; private set; } + public CommunicationGather JsonGather { get; private set; } + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + public BoolFeedback PresentationViewMaximizedFeedback { get; private set; } + + string CurrentPresentationView; + + public BoolFeedback RoomIsOccupiedFeedback { get; private set; } + + public IntFeedback PeopleCountFeedback { get; private set; } + + public BoolFeedback SpeakerTrackIsOnFeedback { get; private set; } + + public BoolFeedback SelfviewIsOnFeedback { get; private set; } + + public StringFeedback SelfviewPipPositionFeedback { get; private set; } + + public StringFeedback LocalLayoutFeedback { 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; } + + public CodecDirectory DirectoryRoot { 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 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; + } + } + + + 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 + + int PhonebookResultsLimit = 255; // Could be set later by config. + + CTimer LoginMessageReceived; + + // **___________________________________________________________________** + // 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(string key, string name, IBasicCommunication comm, CiscoSparkCodecPropertiesConfig props ) + : base(key, name) + { + RoomIsOccupiedFeedback = new BoolFeedback(RoomIsOccupiedFeedbackFunc); + PeopleCountFeedback = new IntFeedback(PeopleCountFeedbackFunc); + SpeakerTrackIsOnFeedback = new BoolFeedback(SpeakerTrackIsOnFeedbackFunc); + SelfviewIsOnFeedback = new BoolFeedback(SelfViewIsOnFeedbackFunc); + SelfviewPipPositionFeedback = new StringFeedback(SelfviewPipPositionFeedbackFunc); + LocalLayoutFeedback = new StringFeedback(LocalLayoutFeedbackFunc); +<<<<<<< HEAD +======= + LocalLayoutIsProminentFeedback = new BoolFeedback(LocalLayoutIsProminentFeedbackFunc); + FarEndIsSharingContentFeedback = new BoolFeedback(FarEndIsSharingContentFeedbackFunc); +>>>>>>> development + + 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(); + + CodecSchedule = new CodecScheduleAwareness(); + + //Set Feedback Actions + 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 = SpeakerTrackIsOnFeedback.FireUpdate; + CodecStatus.Status.Video.Selfview.Mode.ValueChangedAction = SelfviewIsOnFeedback.FireUpdate; + CodecStatus.Status.Video.Selfview.PIPPosition.ValueChangedAction = ComputeSelfviewPipStatus; + CodecStatus.Status.Video.Layout.LayoutFamily.Local.ValueChangedAction = ComputeLocalLayout; +<<<<<<< HEAD + CodecStatus.Status.Conference.Presentation.Mode.ValueChangedAction = SharingContentIsOnFeedback.FireUpdate; +======= + CodecStatus.Status.Conference.Presentation.Mode.ValueChangedAction += SharingContentIsOnFeedback.FireUpdate; + CodecStatus.Status.Conference.Presentation.Mode.ValueChangedAction += FarEndIsSharingContentFeedback.FireUpdate; +>>>>>>> development + + CodecOsdIn = new RoutingInputPort(RoutingPortNames.CodecOsd, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(StopSharing), this); + HdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(SelectPresentationSource1), this); + HdmiIn3 = new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(SelectPresentationSource2), this); + + HdmiOut1 = new RoutingOutputPort(RoutingPortNames.HdmiOut1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + HdmiOut2 = new RoutingOutputPort(RoutingPortNames.HdmiOut2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + InputPorts.Add(CodecOsdIn); + InputPorts.Add(HdmiIn2); + InputPorts.Add(HdmiIn3); + OutputPorts.Add(HdmiOut1); + + CreateOsdSource(); + } + + + /// + /// 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); + } + + /// + /// 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); + + Communication.Connect(); + LoginMessageReceived = new CTimer(DisconnectClientAndReconnect, 5000); + + var socket = Communication as ISocketStatus; + if (socket != null) + { + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + + CommunicationMonitor.Start(); + + 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/Standby" + Delimiter + + prefix + "/Status/Video/Selfview" + Delimiter + + prefix + "/Status/Video/Layout" + Delimiter + + prefix + "/Bookings" + Delimiter + + prefix + "/Event/CallDisconnect" + Delimiter + + prefix + "/Event/Bookings" + Delimiter; + + return base.CustomActivate(); + } + + /// + /// 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) + { + if (e.Client.IsConnected) + { + LoginMessageReceived.Reset(5000); + } + else + { + SyncState.CodecDisconnected(); + PhonebookSyncState.CodecDisconnected(); + + if (PhonebookRefreshTimer != null) + { + PhonebookRefreshTimer.Stop(); + PhonebookRefreshTimer = null; + } + + if (BookingsRefreshTimer != null) + { + BookingsRefreshTimer.Stop(); + BookingsRefreshTimer = null; + } + } + } + + void DisconnectClientAndReconnect(object o) + { + Debug.Console(0, this, "Disconnecting and Reconnecting to codec."); + + Communication.Disconnect(); + + 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()); + + // Forward the complete message to be deserialized + DeserializeResponse(JsonMessage.ToString()); + 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": + { + LoginMessageReceived.Stop(); + SendText("xPreferences outputmode json"); + break; + } + case "xpreferences outputmode json": + { + if (!SyncState.InitialStatusMessageWasReceived) + SendText("xStatus"); + break; + } + case "xfeedback register /event/calldisconnect": + { + SyncState.FeedbackRegistered(); + break; + } + } + } + + } + + 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 mising members + JsonSerializerSettings settings = new JsonSerializerSettings(); + settings.NullValueHandling = NullValueHandling.Ignore; + settings.MissingMemberHandling = MissingMemberHandling.Ignore; + settings.ObjectCreationHandling = ObjectCreationHandling.Auto; + + if (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); + } + + } + + } + + JsonConvert.PopulateObject(response, CodecStatus); + + if (!SyncState.InitialStatusMessageWasReceived) + { + SyncState.InitialStatusMessageReceived(); + + if (!SyncState.InitialConfigurationMessageWasReceived) + SendText("xConfiguration"); + } + } + else if (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) + { + if (response.IndexOf("\"CallDisconnect\":{") > -1) + { + CiscoCodecEvents.RootObject eventReceived = new CiscoCodecEvents.RootObject(); + + JsonConvert.PopulateObject(response, eventReceived); + + EvalutateDisconnectEvent(eventReceived); + } + else if (response.IndexOf("\"Bookings\":{") > -1) // The list has changed, reload it + { + GetBookings(null); + } + } + else if (response.IndexOf("\"CommandResponse\":{") > -1) + { + // CommandResponse Message + + if (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) + { + GetCallHistory(); + } + else if (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(); + + PrintPhonebook(DirectoryRoot); + } + else if (PhonebookSyncState.InitialSyncComplete) + { + var directoryResults = new CodecDirectory(); + + if(codecPhonebookResponse.CommandResponse.PhonebookSearchResult.ResultInfo.TotalRows.Value != "0") + directoryResults = CiscoCodecPhonebook.ConvertCiscoPhonebookToGeneric(codecPhonebookResponse.CommandResponse.PhonebookSearchResult); + + PrintPhonebook(directoryResults); + + // 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 = directoryResults }); + + // Fire some sort of callback delegate to the UI that requested the directory search results + } + } + 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); + } + } + + /// + /// 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 first CallId or returns null + /// + /// + private string GetCallId() + { + string callId = null; + + if (ActiveCalls.Count > 1) + { + foreach (CodecActiveCallItem call in ActiveCalls) ; + } + 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)); + } + + void PrintPhonebook(CodecDirectory directory) + { + if (Debug.Level > 0) + { + Debug.Console(1, this, "Directory Results:\n"); + + foreach (DirectoryItem item in directory.DirectoryResults) + { + if (item is DirectoryFolder) + { + Debug.Console(1, this, "[+] {0}", item.Name); + } + else if (item is DirectoryContact) + { + Debug.Console(1, this, "{0}", item.Name); + } + } + } + } + + /// + /// 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) + { + if (CallFavorites != null) + { + 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"); + } + + /// + /// 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]); + } + } + + /// + /// + /// + 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)); + } + + public class CiscoCodecInfo : VideoCodecInfo + { + public CiscoCodecStatus.RootObject CodecStatus { get; private set; } + + public CiscoCodecConfiguration.RootObject CodecConfiguration { get; private set; } + + public override bool MultiSiteOptionIsEnabled + { + get + { + if (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.H323Alias.E164 != null) + return CodecConfiguration.Configuration.H323.H323Alias.E164.Value; + else + return string.Empty; + } + } + public override string H323Id + { + get + { + if (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.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.AlternateURI.Primary.URI.Value != null) + return CodecStatus.Status.SIP.AlternateURI.Primary.URI.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; + } + } + } + + /// + /// 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 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 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() + { + InitialConfigurationMessageWasReceived = false; + InitialStatusMessageWasReceived = false; + FeedbackWasRegistered = false; + InitialSyncComplete = false; + } + + void CheckSyncStatus() + { + if (InitialConfigurationMessageWasReceived && InitialStatusMessageWasReceived && FeedbackWasRegistered) + { + InitialSyncComplete = true; + Debug.Console(1, this, "Initial Codec Sync Complete!"); + } + else + InitialSyncComplete = false; + } + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodecPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodecPropertiesConfig.cs new file mode 100644 index 00000000..33cda37c --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodecPropertiesConfig.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.Codec +{ + public class CiscoSparkCodecPropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + + public List Favorites { get; set; } + + /// + /// Valid values: "Local" or "Corporate" + /// + public string PhonebookMode { get; set; } + + public bool ShowSelfViewByDefault { get; set; } + + public SharingProperties Sharing { get; set; } + + } + + public class SharingProperties + { + public bool AutoShareContentWhileInCall { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/HttpApiServer.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/HttpApiServer.cs new file mode 100644 index 00000000..affe9c2f --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/HttpApiServer.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.Net.Http; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco +{ + public class HttpApiServer + { + public static Dictionary ExtensionContentTypes; + + public event EventHandler ApiRequest; + public Crestron.SimplSharp.Net.Http.HttpServer HttpServer { get; private set; } + + public string HtmlRoot { get; set; } + + + /// + /// SIMPL+ can only execute the default constructor. If you have variables that require initialization, please + /// use an Initialize method + /// + public HttpApiServer() + { + ExtensionContentTypes = new Dictionary + { + { ".css", "text/css" }, + { ".htm", "text/html" }, + { ".html", "text/html" }, + { ".jpg", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".js", "application/javascript" }, + { ".json", "application/json" }, + { ".xml", "text/xml" }, + { ".map", "application/x-navimap" }, + { ".pdf", "application.pdf" }, + { ".png", "image/png" }, + { ".txt", "text/plain" }, + }; + HtmlRoot = @"\HTML"; + } + + + public void Start(int port) + { + // TEMP - this should be inserted by configuring class + + HttpServer = new Crestron.SimplSharp.Net.Http.HttpServer(); + HttpServer.ServerName = "Cisco API Server"; + HttpServer.KeepAlive = true; + HttpServer.Port = port; + HttpServer.OnHttpRequest += Server_Request; + HttpServer.Open(); + + CrestronEnvironment.ProgramStatusEventHandler += (a) => + { + if (a == eProgramStatusEventType.Stopping) + { + HttpServer.Close(); + Debug.Console(1, "Shutting down HTTP Server on port {0}", HttpServer.Port); + } + }; + } + + void Server_Request(object sender, OnHttpRequestArgs args) + { + if (args.Request.Header.RequestType == "OPTIONS") + { + Debug.Console(2, "Asking for OPTIONS"); + args.Response.Header.SetHeaderValue("Access-Control-Allow-Origin", "*"); + args.Response.Header.SetHeaderValue("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS"); + return; + } + + string path = Uri.UnescapeDataString(args.Request.Path); + var host = args.Request.DataConnection.RemoteEndPointAddress; + //string authToken; + + Debug.Console(2, "HTTP Request: {2}: Path='{0}' ?'{1}'", path, args.Request.QueryString, host); + + // ----------------------------------- ADD AUTH HERE + if (path.StartsWith("/cisco/api")) + { + var handler = ApiRequest; + if (ApiRequest != null) + ApiRequest(this, args); + } + } + + public static string GetContentType(string extension) + { + string type; + if (ExtensionContentTypes.ContainsKey(extension)) + type = ExtensionContentTypes[extension]; + else + type = "text/plain"; + return type; + } + + } + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/PhonebookDataClasses.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/PhonebookDataClasses.cs new file mode 100644 index 00000000..c88261f5 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/PhonebookDataClasses.cs @@ -0,0 +1,397 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + public class CiscoCodecPhonebook + { + public class Offset + { + public string Value { get; set; } + } + + public class Limit + { + public string Value { get; set; } + } + + public class TotalRows + { + public string Value { get; set; } + } + + public class ResultInfo + { + public Offset Offset { get; set; } + public Limit Limit { get; set; } + public TotalRows TotalRows { get; set; } + } + + public class LocalId + { + public string Value { get; set; } + } + + public class FolderId + { + public string Value { get; set; } + } + + public class ParentFolderId + { + public string Value { get; set; } + } + + public class Name + { + public string Value { get; set; } + } + + public class Folder + { + public string id { get; set; } + public LocalId LocalId { get; set; } + public FolderId FolderId { get; set; } + public Name Name { get; set; } + public ParentFolderId ParentFolderId { get; set; } + } + + public class Name2 + { + public string Value { get; set; } + } + + public class ContactId + { + public string Value { get; set; } + } + + public class FolderId2 + { + public string Value { get; set; } + } + + public class Title + { + public string Value { get; set; } + } + + public class ContactMethodId + { + public string Value { get; set; } + } + + public class Number + { + public string Value { get; set; } + } + + public class Device + { + public string Value { get; set; } + } + + public class CallType + { + public string Value { get; set; } + } + + public class ContactMethod + { + public string id { get; set; } + public ContactMethodId ContactMethodId { get; set; } + public Number Number { get; set; } + public Device Device { get; set; } + public CallType CallType { get; set; } + + public ContactMethod() + { + ContactMethodId = new ContactMethodId(); + Number = new Number(); + Device = new Device(); + CallType = new CallType(); + } + } + + public class Contact + { + public string id { get; set; } + public Name2 Name { get; set; } + public ContactId ContactId { get; set; } + public FolderId2 FolderId { get; set; } + public Title Title { get; set; } + public List ContactMethod { get; set; } + + public Contact() + { + Name = new Name2(); + ContactId = new ContactId(); + FolderId = new FolderId2(); + Title = new Title(); + ContactMethod = new List(); + } + } + + public class PhonebookSearchResult + { + public string status { get; set; } + public ResultInfo ResultInfo { get; set; } + public List Folder { get; set; } + public List Contact { get; set; } + + public PhonebookSearchResult() + { + Folder = new List(); + Contact = new List(); + ResultInfo = new ResultInfo(); + } + } + + public class CommandResponse + { + public PhonebookSearchResult PhonebookSearchResult { get; set; } + } + + public class RootObject + { + public CommandResponse CommandResponse { get; set; } + + } + + + /// + /// Extracts the folders with no ParentFolder and returns them sorted alphabetically + /// + /// + /// + public static List GetRootFoldersFromSearchResult(PhonebookSearchResult result) + { + var rootFolders = new List(); + + if (result.Folder.Count == 0) + { + return null; + } + else if (result.Folder.Count > 0) + { + if (Debug.Level > 0) + Debug.Console(1, "Phonebook Folders:\n"); + + foreach (Folder f in result.Folder) + { + var folder = new DirectoryFolder(); + + folder.Name = f.Name.Value; + folder.FolderId = f.FolderId.Value; + + if (f.ParentFolderId == null) + rootFolders.Add(folder); + + if (Debug.Level > 0) + Debug.Console(1, "+ {0}", folder.Name); + } + } + + rootFolders.OrderBy(f => f.Name); + + return rootFolders; + } + + + /// + /// Extracts the contacts with no FolderId and returns them sorted alphabetically + /// + /// + /// + public static List GetRootContactsFromSearchResult(PhonebookSearchResult result) + { + var rootContacts = new List(); + + if (result.Contact.Count == 0) + { + return null; + } + else if (result.Contact.Count > 0) + { + if (Debug.Level > 0) + Debug.Console(1, "Root Contacts:\n"); + + foreach (Contact c in result.Contact) + { + var contact = new DirectoryContact(); + + if (string.IsNullOrEmpty(c.FolderId.Value)) + { + contact.Name = c.Name.Value; + contact.ContactId = c.ContactId.Value; + + if(!string.IsNullOrEmpty(c.Title.Value)) + contact.Title = c.Title.Value; + + if (Debug.Level > 0) + Debug.Console(1, "{0}\nContact Methods:", contact.Name); + + foreach (ContactMethod m in c.ContactMethod) + { + + var tempContactMethod = new PepperDash.Essentials.Devices.Common.Codec.ContactMethod(); + + eContactMethodCallType callType = eContactMethodCallType.Unknown; + if (!string.IsNullOrEmpty(m.CallType.Value)) + { + if (!string.IsNullOrEmpty(m.CallType.Value)) + { + if (m.CallType.Value.ToLower() == "audio") + callType = eContactMethodCallType.Audio; + else if (m.CallType.Value.ToLower() == "video") + callType = eContactMethodCallType.Video; + + tempContactMethod.CallType = callType; + } + } + + eContactMethodDevice device = eContactMethodDevice.Unknown; + if (!string.IsNullOrEmpty(m.Device.Value)) + { + + if (m.Device.Value.ToLower() == "mobile") + device = eContactMethodDevice.Mobile; + else if (m.Device.Value.ToLower() == "telephone") + device = eContactMethodDevice.Telephone; + else if (m.Device.Value.ToLower() == "video") + device = eContactMethodDevice.Video; + else if (m.Device.Value.ToLower() == "other") + device = eContactMethodDevice.Other; + + tempContactMethod.Device = device; + } + + if (Debug.Level > 0) + Debug.Console(1, "Number: {0}", m.Number.Value); + + tempContactMethod.Number = m.Number.Value; + tempContactMethod.ContactMethodId = m.ContactMethodId.Value; + + contact.ContactMethods.Add(tempContactMethod); + } + rootContacts.Add(contact); + } + } + } + + rootContacts.OrderBy(f => f.Name); + + return rootContacts; + } + + + /// + /// Converts data returned from a cisco codec to the generic Directory format. + /// + /// + /// + /// + public static CodecDirectory ConvertCiscoPhonebookToGeneric(PhonebookSearchResult result) + { + var directory = new Codec.CodecDirectory(); + + var folders = new List(); + + var contacts = new List(); + + try + { + if (result.Folder.Count > 0) + { + foreach (Folder f in result.Folder) + { + var folder = new DirectoryFolder(); + + folder.Name = f.Name.Value; + folder.FolderId = f.FolderId.Value; + + if (f.ParentFolderId != null) + { + folder.ParentFolderId = f.ParentFolderId.Value; + } + + folders.Add(folder); + } + + folders.OrderBy(f => f.Name); + + directory.AddFoldersToDirectory(folders); + } + + if (result.Contact.Count > 0) + { + foreach (Contact c in result.Contact) + { + var contact = new DirectoryContact(); + + contact.Name = c.Name.Value; + contact.ContactId = c.ContactId.Value; + if (!string.IsNullOrEmpty(c.Title.Value)) + contact.Title = c.Title.Value; + + if (c.FolderId != null) + { + contact.FolderId = c.FolderId.Value; + } + + foreach (ContactMethod m in c.ContactMethod) + { + eContactMethodCallType callType = eContactMethodCallType.Unknown; + if (!string.IsNullOrEmpty(m.CallType.Value)) + { + if (m.CallType.Value.ToLower() == "audio") + callType = eContactMethodCallType.Audio; + else if (m.CallType.Value.ToLower() == "video") + callType = eContactMethodCallType.Video; + } + + eContactMethodDevice device = eContactMethodDevice.Unknown; + + if (!string.IsNullOrEmpty(m.Device.Value)) + { + if (m.Device.Value.ToLower() == "mobile") + device = eContactMethodDevice.Mobile; + else if (m.Device.Value.ToLower() == "telephone") + device = eContactMethodDevice.Telephone; + else if (m.Device.Value.ToLower() == "video") + device = eContactMethodDevice.Video; + else if (m.Device.Value.ToLower() == "other") + device = eContactMethodDevice.Other; + } + + contact.ContactMethods.Add(new PepperDash.Essentials.Devices.Common.Codec.ContactMethod() + { + Number = m.Number.Value, + ContactMethodId = m.ContactMethodId.Value, + CallType = callType, + Device = device + }); + } + contacts.Add(contact); + } + + contacts.OrderBy(c => c.Name); + + directory.AddContactsToDirectory(contacts); + } + } + catch (Exception e) + { + Debug.Console(1, "Error converting Cisco Phonebook results to generic: {0}", e); + } + + return directory; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/RoomPresets.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/RoomPresets.cs new file mode 100644 index 00000000..2846674a --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/RoomPresets.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using Newtonsoft.Json; + +using PepperDash.Core; +using PepperDash.Essentials.Devices.Common.VideoCodec.Cisco; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + /// + /// Interface for camera presets + /// + public interface IHasCodecRoomPresets + { + event EventHandler CodecRoomPresetsListHasChanged; + + List NearEndPresets { get; } + + List FarEndRoomPresets { get; } + + void CodecRoomPresetSelect(int preset); + + void CodecRoomPresetStore(int preset, string description); + } + + public static class RoomPresets + { + /// + /// Converts Cisco RoomPresets to generic CameraPresets + /// + /// + /// + public static List GetGenericPresets(List presets) + { + var cameraPresets = new List(); + + if (Debug.Level > 0) + { + Debug.Console(1, "Presets List:"); + } + + foreach (CiscoCodecStatus.RoomPreset preset in presets) + { + try + { + var cameraPreset = new CodecRoomPreset(UInt16.Parse(preset.id), preset.Description.Value, preset.Defined.BoolValue, true); + + cameraPresets.Add(cameraPreset); + + if (Debug.Level > 0) + { + Debug.Console(1, "Added Preset ID: {0}, Description: {1}, IsDefined: {2}, isDefinable: {3}", cameraPreset.ID, cameraPreset.Description, cameraPreset.Defined, cameraPreset.IsDefinable); + } + } + catch (Exception e) + { + Debug.Console(2, "Unable to convert preset: {0}. Error: {1}", preset.id, e); + } + } + + return cameraPresets; + } + } + + /// + /// Represents a room preset on a video coded. Typically stores camera position(s) and video routing. Can be recalled by Far End if enabled. + /// + public class CodecRoomPreset + { + [JsonProperty("id")] + public int ID { get; set; } + /// + /// Used to store the name of the preset + /// + [JsonProperty("description")] + public string Description { get; set; } + /// + /// Indicates if the preset is defined(stored) in the codec + /// + [JsonProperty("defined")] + public bool Defined { get; set; } + /// + /// Indicates if the preset has the capability to be defined + /// + [JsonProperty("isDefinable")] + public bool IsDefinable { get; set; } + + public CodecRoomPreset(int id, string description, bool def, bool isDef) + { + ID = id; + Description = description; + Defined = def; + IsDefinable = isDef; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xConfiguration.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xConfiguration.cs new file mode 100644 index 00000000..8ae9e643 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xConfiguration.cs @@ -0,0 +1,1807 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Crestron.SimplSharp; + +using PepperDash.Core; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco +{ + /// + /// This class exists to capture serialized data sent back by a Cisco codec in JSON output mode + /// + public class CiscoCodecConfiguration + { + public class DefaultVolume + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Dereverberation + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class NoiseReduction + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class EchoControl + { + public Dereverberation Dereverberation { get; set; } + public Mode Mode { get; set; } + public NoiseReduction NoiseReduction { get; set; } + } + + public class Level + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Microphone + { + public string id { get; set; } + public EchoControl EchoControl { get; set; } + public Level Level { get; set; } + public Mode2 Mode { get; set; } + } + + public class Input + { + public List Microphone { get; set; } + } + + public class Enabled + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mute + { + public Enabled Enabled { get; set; } + } + + public class Microphones + { + public Mute Mute { get; set; } + } + + public class Mode3 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class InternalSpeaker + { + public Mode3 Mode { get; set; } + } + + public class Output + { + public InternalSpeaker InternalSpeaker { get; set; } + } + + public class RingTone + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class RingVolume + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SoundsAndAlerts + { + public RingTone RingTone { get; set; } + public RingVolume RingVolume { get; set; } + } + + public class Audio + { + public DefaultVolume DefaultVolume { get; set; } + public Input Input { get; set; } + public Microphones Microphones { get; set; } + public Output Output { get; set; } + public SoundsAndAlerts SoundsAndAlerts { get; set; } + } + + public class DefaultMode + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Backlight + { + public DefaultMode DefaultMode { get; set; } + } + + public class DefaultLevel + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode4 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Brightness + { + public DefaultLevel DefaultLevel { get; set; } + public Mode4 Mode { get; set; } + } + + public class Mode5 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Focus + { + public Mode5 Mode { get; set; } + } + + public class Level2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode6 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Gamma + { + public Level2 Level { get; set; } + public Mode6 Mode { get; set; } + } + + public class Mirror + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Level3 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode7 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Whitebalance + { + public Level3 Level { get; set; } + public Mode7 Mode { get; set; } + } + + public class Framerate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Camera + { + public string id { get; set; } + public Framerate Framerate { get; set; } + public Backlight Backlight { get; set; } + public Brightness Brightness { get; set; } + public Focus Focus { get; set; } + public Gamma Gamma { get; set; } + public Mirror Mirror { get; set; } + public Whitebalance Whitebalance { get; set; } + } + + public class Closeup + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode8 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SpeakerTrack + { + public Closeup Closeup { get; set; } + public Mode8 Mode { get; set; } + } + + public class Cameras + { + //[JsonConverter(typeof(CameraConverter)), JsonProperty("Camera")] + //public List Camera { get; set; } + //[JsonProperty("SpeakerTrack")] + public SpeakerTrack SpeakerTrack { get; set; } + + public Cameras() + { + //Camera = new List(); + SpeakerTrack = new SpeakerTrack(); + } + } + + public class CameraConverter : JsonConverter + { + // this is currently not working + public override bool CanConvert(System.Type objectType) + { + return objectType == typeof(Camera) || objectType == typeof(List); // This should not be called but is required for implmentation + } + + public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer) + { + try + { + if (reader.TokenType == JsonToken.StartArray) + { + var l = new List(); + reader.Read(); + while (reader.TokenType != JsonToken.EndArray) + { + l.Add(reader.Value as Camera); + reader.Read(); + } + Debug.Console(1, "[xConfiguration]: Cameras converted as list"); + return l; + } + else + { + Debug.Console(1, "[xConfiguration]: Camera converted as single object and added to list"); + return new List { reader.Value as Camera }; + } + } + catch (Exception e) + { + Debug.Console(1, "[xConfiguration]: Unable to convert JSON for camera objects: {0}", e); + + return new List(); + } + } + + public override bool CanWrite + { + get + { + return false; + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException("Write not implemented"); + } + } + + public class Delay + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode9 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mute2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class AutoAnswer + { + public Delay Delay { get; set; } + public Mode9 Mode { get; set; } + public Mute2 Mute { get; set; } + } + + public class Protocol + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Rate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class DefaultCall + { + public Protocol Protocol { get; set; } + public Rate Rate { get; set; } + } + + public class DefaultTimeout + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class DoNotDisturb + { + public DefaultTimeout DefaultTimeout { get; set; } + } + + public class Mode10 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Encryption + { + public Mode10 Mode { get; set; } + } + + public class Mode11 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class FarEndControl + { + public Mode11 Mode { get; set; } + } + + public class MaxReceiveCallRate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class MaxTotalReceiveCallRate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class MaxTotalTransmitCallRate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class MaxTransmitCallRate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode12 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class MultiStream + { + public Mode12 Mode { get; set; } + } + + public class Conference + { + public AutoAnswer AutoAnswer { get; set; } + public DefaultCall DefaultCall { get; set; } + public DoNotDisturb DoNotDisturb { get; set; } + public Encryption Encryption { get; set; } + public FarEndControl FarEndControl { get; set; } + public MaxReceiveCallRate MaxReceiveCallRate { get; set; } + public MaxTotalReceiveCallRate MaxTotalReceiveCallRate { get; set; } + public MaxTotalTransmitCallRate MaxTotalTransmitCallRate { get; set; } + public MaxTransmitCallRate MaxTransmitCallRate { get; set; } + public MultiStream MultiStream { get; set; } + } + + public class LoginName + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode13 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Password + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Authentication + { + public LoginName LoginName { get; set; } + public Mode13 Mode { get; set; } + public Password Password { get; set; } + } + + public class Mode14 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class CallSetup + { + public Mode14 Mode { get; set; } + } + + public class KeySize + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Encryption2 + { + public KeySize KeySize { get; set; } + } + + public class Address + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Gatekeeper + { + public Address Address { get; set; } + } + + public class E164 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class ID + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class H323Alias + { + public E164 E164 { get; set; } + public ID ID { get; set; } + } + + public class Address2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode15 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class NAT + { + public Address2 Address { get; set; } + public Mode15 Mode { get; set; } + } + + public class H323 + { + public Authentication Authentication { get; set; } + public CallSetup CallSetup { get; set; } + public Encryption2 Encryption { get; set; } + public Gatekeeper Gatekeeper { get; set; } + public H323Alias H323Alias { get; set; } + public NAT NAT { get; set; } + } + + public class Name + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Domain + { + public Name Name { get; set; } + } + + public class Address3 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Server + { + public string id { get; set; } + public Address3 Address { get; set; } + } + + public class DNS + { + public Domain Domain { get; set; } + public List Server { get; set; } + } + + public class AnonymousIdentity + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Md5 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Peap + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Tls + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Ttls + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Eap + { + public Md5 Md5 { get; set; } + public Peap Peap { get; set; } + public Tls Tls { get; set; } + public Ttls Ttls { get; set; } + } + + public class Identity + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode16 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Password2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class TlsVerify + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class UseClientCertificate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class IEEE8021X + { + public AnonymousIdentity AnonymousIdentity { get; set; } + public Eap Eap { get; set; } + public Identity Identity { get; set; } + public Mode16 Mode { get; set; } + public Password2 Password { get; set; } + public TlsVerify TlsVerify { get; set; } + public UseClientCertificate UseClientCertificate { get; set; } + } + + public class IPStack + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Address4 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Assignment + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Gateway + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SubnetMask + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class IPv4 + { + public Address4 Address { get; set; } + public Assignment Assignment { get; set; } + public Gateway Gateway { get; set; } + public SubnetMask SubnetMask { get; set; } + } + + public class Address5 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Assignment2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class DHCPOptions + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Gateway2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class IPv6 + { + public Address5 Address { get; set; } + public Assignment2 Assignment { get; set; } + public DHCPOptions DHCPOptions { get; set; } + public Gateway2 Gateway { get; set; } + } + + public class MTU + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Audio2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Data + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class ICMPv6 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class NTP + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Signalling + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Video + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Diffserv + { + public Audio2 Audio { get; set; } + public Data Data { get; set; } + public ICMPv6 ICMPv6 { get; set; } + public NTP NTP { get; set; } + public Signalling Signalling { get; set; } + public Video Video { get; set; } + } + + public class Mode17 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class QoS + { + public Diffserv Diffserv { get; set; } + public Mode17 Mode { get; set; } + } + + public class Allow + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class RemoteAccess + { + public Allow Allow { get; set; } + } + + public class Speed + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode18 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class VlanId + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Voice + { + public Mode18 Mode { get; set; } + public VlanId VlanId { get; set; } + } + + public class VLAN + { + public Voice Voice { get; set; } + } + + public class Network + { + public string id { get; set; } + public DNS DNS { get; set; } + public IEEE8021X IEEE8021X { get; set; } + public IPStack IPStack { get; set; } + public IPv4 IPv4 { get; set; } + public IPv6 IPv6 { get; set; } + public MTU MTU { get; set; } + public QoS QoS { get; set; } + public RemoteAccess RemoteAccess { get; set; } + public Speed Speed { get; set; } + public VLAN VLAN { get; set; } + } + + public class Mode19 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class CDP + { + public Mode19 Mode { get; set; } + } + + public class Mode20 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class H3232 + { + public Mode20 Mode { get; set; } + } + + public class Mode21 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class HTTP + { + public Mode21 Mode { get; set; } + } + + public class MinimumTLSVersion + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Server2 + { + public MinimumTLSVersion MinimumTLSVersion { get; set; } + } + + public class StrictTransportSecurity + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class VerifyClientCertificate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class VerifyServerCertificate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class HTTPS + { + public Server2 Server { get; set; } + public StrictTransportSecurity StrictTransportSecurity { get; set; } + public VerifyClientCertificate VerifyClientCertificate { get; set; } + public VerifyServerCertificate VerifyServerCertificate { get; set; } + } + + public class Mode22 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Address6 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Server3 + { + public string id { get; set; } + public Address6 Address { get; set; } + } + + public class NTP2 + { + public Mode22 Mode { get; set; } + public List Server { get; set; } + } + + public class Mode23 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SIP + { + public Mode23 Mode { get; set; } + } + + public class CommunityName + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Address7 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Host + { + public string id { get; set; } + public Address7 Address { get; set; } + } + + public class Mode24 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SystemContact + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SystemLocation + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SNMP + { + public CommunityName CommunityName { get; set; } + public List Host { get; set; } + public Mode24 Mode { get; set; } + public SystemContact SystemContact { get; set; } + public SystemLocation SystemLocation { get; set; } + } + + public class Mode25 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SSH + { + public Mode25 Mode { get; set; } + } + + public class Mode26 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class UPnP + { + public Mode26 Mode { get; set; } + } + + public class WelcomeText + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class NetworkServices + { + public CDP CDP { get; set; } + public H3232 H323 { get; set; } + public HTTP HTTP { get; set; } + public HTTPS HTTPS { get; set; } + public NTP2 NTP { get; set; } + public SIP SIP { get; set; } + public SNMP SNMP { get; set; } + public SSH SSH { get; set; } + public UPnP UPnP { get; set; } + public WelcomeText WelcomeText { get; set; } + } + + public class Cameras2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class ControlSystems + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class TouchPanels + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Profile + { + public Cameras2 Cameras { get; set; } + public ControlSystems ControlSystems { get; set; } + public TouchPanels TouchPanels { get; set; } + } + + public class Peripherals + { + public Profile Profile { get; set; } + } + + public class ID2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Type + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class URL + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Server4 + { + public string id { get; set; } + public ID2 ID { get; set; } + public Type Type { get; set; } + public URL URL { get; set; } + } + + public class Phonebook + { + public List Server { get; set; } + } + + public class Connectivity + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Address8 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class AlternateAddress + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Domain2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Path + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Protocol2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class ExternalManager + { + public Address8 Address { get; set; } + public AlternateAddress AlternateAddress { get; set; } + public Domain2 Domain { get; set; } + public Path Path { get; set; } + public Protocol2 Protocol { get; set; } + } + + public class HttpMethod + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class LoginName2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode27 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Password3 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Provisioning + { + public Connectivity Connectivity { get; set; } + public ExternalManager ExternalManager { get; set; } + public HttpMethod HttpMethod { get; set; } + public LoginName2 LoginName { get; set; } + public Mode27 Mode { get; set; } + public Password3 Password { get; set; } + } + + public class Mode28 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class CallControl + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class FromClients + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class ToClients + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class ContentShare + { + public FromClients FromClients { get; set; } + public ToClients ToClients { get; set; } + } + + public class Services + { + public CallControl CallControl { get; set; } + public ContentShare ContentShare { get; set; } + } + + public class Proximity + { + public Mode28 Mode { get; set; } + public Services Services { get; set; } + } + + public class PeopleCountOutOfCall + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class PeoplePresenceDetector + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class RoomAnalytics + { + public PeopleCountOutOfCall PeopleCountOutOfCall { get; set; } + public PeoplePresenceDetector PeoplePresenceDetector { get; set; } + } + + public class Password4 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class UserName + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Authentication2 + { + public Password4 Password { get; set; } + public UserName UserName { get; set; } + } + + public class DefaultTransport + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class DisplayName + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class DefaultCandidate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode29 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Ice + { + public DefaultCandidate DefaultCandidate { get; set; } + public Mode29 Mode { get; set; } + } + + public class ListenPort + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Address9 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Proxy + { + public string id { get; set; } + public Address9 Address { get; set; } + } + + public class Password5 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Server5 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class UserName2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Turn + { + public Password5 Password { get; set; } + public Server5 Server { get; set; } + public UserName2 UserName { get; set; } + } + + public class URI + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SIP2 + { + public Authentication2 Authentication { get; set; } + public DefaultTransport DefaultTransport { get; set; } + public DisplayName DisplayName { get; set; } + public Ice Ice { get; set; } + public ListenPort ListenPort { get; set; } + public List Proxy { get; set; } + public Turn Turn { get; set; } + public URI URI { get; set; } + } + + public class BaudRate + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class LoginRequired + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode30 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SerialPort + { + public BaudRate BaudRate { get; set; } + public LoginRequired LoginRequired { get; set; } + public Mode30 Mode { get; set; } + } + + public class BootAction + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Control + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Delay2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class StandbyAction + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class WakeupAction + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Standby + { + public BootAction BootAction { get; set; } + public Control Control { get; set; } + public Delay2 Delay { get; set; } + public StandbyAction StandbyAction { get; set; } + public WakeupAction WakeupAction { get; set; } + } + + public class Name2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class SystemUnit + { + public Name2 Name { get; set; } + } + + public class DateFormat + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class TimeFormat + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Zone + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Time + { + public DateFormat DateFormat { get; set; } + public TimeFormat TimeFormat { get; set; } + public Zone Zone { get; set; } + } + + public class Type2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class ContactInfo + { + public Type2 Type { get; set; } + } + + public class Mode31 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class KeyTones + { + public Mode31 Mode { get; set; } + } + + public class Language + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Output2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class OSD + { + public Output2 Output { get; set; } + } + + public class UserInterface + { + public ContactInfo ContactInfo { get; set; } + public KeyTones KeyTones { get; set; } + public Language Language { get; set; } + public OSD OSD { get; set; } + } + + public class Filter + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Group + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Admin + { + public Filter Filter { get; set; } + public Group Group { get; set; } + } + + public class Attribute + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class BaseDN + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Encryption3 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class MinimumTLSVersion2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode32 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Address10 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Port + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Server6 + { + public Address10 Address { get; set; } + public Port Port { get; set; } + } + + public class VerifyServerCertificate2 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class LDAP + { + public Admin Admin { get; set; } + public Attribute Attribute { get; set; } + public BaseDN BaseDN { get; set; } + public Encryption3 Encryption { get; set; } + public MinimumTLSVersion2 MinimumTLSVersion { get; set; } + public Mode32 Mode { get; set; } + public Server6 Server { get; set; } + public VerifyServerCertificate2 VerifyServerCertificate { get; set; } + } + + public class UserManagement + { + public LDAP LDAP { get; set; } + } + + public class DefaultMainSource + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class CameraId + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode33 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class CameraControl + { + public CameraId CameraId { get; set; } + public Mode33 Mode { get; set; } + } + + public class InputSourceType + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Name3 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class PreferredResolution + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class PresentationSelection + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Quality + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Visibility + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Connector + { + public string id { get; set; } + public CameraControl CameraControl { get; set; } + public InputSourceType InputSourceType { get; set; } + public Name3 Name { get; set; } + public PreferredResolution PreferredResolution { get; set; } + public PresentationSelection PresentationSelection { get; set; } + public Quality Quality { get; set; } + public Visibility Visibility { get; set; } + } + + public class Input2 + { + public List Connector { get; set; } + } + + public class Monitors + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode34 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class CEC + { + public Mode34 Mode { get; set; } + } + + public class MonitorRole + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Resolution + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Connector2 + { + public string id { get; set; } + public CEC CEC { get; set; } + public MonitorRole MonitorRole { get; set; } + public Resolution Resolution { get; set; } + } + + public class Output3 + { + public List Connector { get; set; } + } + + public class DefaultSource + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Presentation + { + public DefaultSource DefaultSource { get; set; } + } + + public class FullscreenMode + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode35 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class OnMonitorRole + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class PIPPosition + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Default + { + public FullscreenMode FullscreenMode { get; set; } + public Mode35 Mode { get; set; } + public OnMonitorRole OnMonitorRole { get; set; } + public PIPPosition PIPPosition { get; set; } + } + + public class Duration + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class Mode36 + { + public string valueSpaceRef { get; set; } + public string Value { get; set; } + } + + public class OnCall + { + public Duration Duration { get; set; } + public Mode36 Mode { get; set; } + } + + public class Selfview + { + public Default Default { get; set; } + public OnCall OnCall { get; set; } + } + + public class Video2 + { + public DefaultMainSource DefaultMainSource { get; set; } + public Input2 Input { get; set; } + public Monitors Monitors { get; set; } + public Output3 Output { get; set; } + public Presentation Presentation { get; set; } + public Selfview Selfview { get; set; } + } + + public class Configuration + { + public Audio Audio { get; set; } + public Cameras Cameras { get; set; } + public Conference Conference { get; set; } + public H323 H323 { get; set; } + public List Network { get; set; } + public NetworkServices NetworkServices { get; set; } + public Peripherals Peripherals { get; set; } + public Phonebook Phonebook { get; set; } + public Provisioning Provisioning { get; set; } + public Proximity Proximity { get; set; } + public RoomAnalytics RoomAnalytics { get; set; } + public SIP2 SIP { get; set; } + public SerialPort SerialPort { get; set; } + public Standby Standby { get; set; } + public SystemUnit SystemUnit { get; set; } + public Time Time { get; set; } + public UserInterface UserInterface { get; set; } + public UserManagement UserManagement { get; set; } + public Video2 Video { get; set; } + } + + public class RootObject + { + public Configuration Configuration { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xEvent.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xEvent.cs new file mode 100644 index 00000000..342a8848 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xEvent.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco +{ + /// + /// This class exists to capture serialized data sent back by a Cisco codec in JSON output mode + /// + public class CiscoCodecEvents + { + public class CauseValue + { + public string id { get; set; } + public string Value { get; set; } + } + + public class CauseType + { + public string id { get; set; } + public string Value { get; set; } + } + + public class CauseString + { + public string id { get; set; } + public string Value { get; set; } + } + + public class OrigCallDirection + { + public string id { get; set; } + public string Value { get; set; } + } + + public class RemoteURI + { + public string id { get; set; } + public string Value { get; set; } + } + + public class DisplayName + { + public string id { get; set; } + public string Value { get; set; } + } + + public class CallId + { + public string id { get; set; } + public string Value { get; set; } + } + + public class CauseCode + { + public string id { get; set; } + public string Value { get; set; } + } + + public class CauseOrigin + { + public string id { get; set; } + public string Value { get; set; } + } + + public class Protocol + { + public string id { get; set; } + public string Value { get; set; } + } + + public class Duration + { + public string id { get; set; } + public string Value { get; set; } + } + + public class CallType + { + public string id { get; set; } + public string Value { get; set; } + } + + public class CallRate + { + public string id { get; set; } + public string Value { get; set; } + } + + public class Encryption + { + public string id { get; set; } + public string Value { get; set; } + } + + public class RequestedURI + { + public string id { get; set; } + public string Value { get; set; } + } + + public class PeopleCountAverage + { + public string id { get; set; } + public string Value { get; set; } + } + + public class CallDisconnect + { + public string id { get; set; } + public CauseValue CauseValue { get; set; } + public CauseType CauseType { get; set; } + public CauseString CauseString { get; set; } + public OrigCallDirection OrigCallDirection { get; set; } + public RemoteURI RemoteURI { get; set; } + public DisplayName DisplayName { get; set; } + public CallId CallId { get; set; } + public CauseCode CauseCode { get; set; } + public CauseOrigin CauseOrigin { get; set; } + public Protocol Protocol { get; set; } + public Duration Duration { get; set; } + public CallType CallType { get; set; } + public CallRate CallRate { get; set; } + public Encryption Encryption { get; set; } + public RequestedURI RequestedURI { get; set; } + public PeopleCountAverage PeopleCountAverage { get; set; } + } + + public class Event + { + public CallDisconnect CallDisconnect { get; set; } + } + + public class RootObject + { + public Event Event { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xStatus.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xStatus.cs new file mode 100644 index 00000000..9af52bd2 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xStatus.cs @@ -0,0 +1,2066 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronXml.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using PepperDash.Core; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco +{ + /// + /// This class exists to capture serialized data sent back by a Cisco codec in JSON output mode + /// + public class CiscoCodecStatus + { + // Helper Classes for Proerties + public abstract class ValueProperty + { + /// + /// Triggered when Value is set + /// + public Action ValueChangedAction { get; set; } + + protected void OnValueChanged() + { + var a = ValueChangedAction; + if (a != null) + a(); + } + + } + + + public class ConnectionStatus + { + public string Value { get; set; } + } + + public class EcReferenceDelay + { + public string Value { get; set; } + } + + public class Microphone + { + public string id { get; set; } + public ConnectionStatus ConnectionStatus { get; set; } + public EcReferenceDelay EcReferenceDelay { get; set; } + } + + public class Connectors + { + public List Microphone { get; set; } + } + + public class Input + { + public Connectors Connectors { get; set; } + } + + public class Mute : ValueProperty + { + public bool BoolValue { get; private set; } + + public string Value + { + set + { + // If the incoming value is "On" it sets the BoolValue true, otherwise sets it false + BoolValue = value == "On"; + OnValueChanged(); + } + } + } + + public class Microphones + { + public Mute Mute { get; set; } + + public Microphones() + { + Mute = new Mute(); + } + } + + public class ConnectionStatus2 + { + public string Value { get; set; } + } + + public class DelayMs + { + public string Value { get; set; } + } + + public class Line + { + public string id { get; set; } + public ConnectionStatus2 ConnectionStatus { get; set; } + public DelayMs DelayMs { get; set; } + } + + public class Connectors2 + { + public List Line { get; set; } + } + + public class Output + { + public Connectors2 Connectors { get; set; } + } + + public class Volume : ValueProperty + { + string _Value; + + /// + /// Sets Value and triggers the action when set + /// + public string Value + { + get + { + return _Value; + } + set + { + _Value = value; + OnValueChanged(); + } + } + + /// + /// Converted value of _Value for use as feedback + /// + public int IntValue + { + get + { + if (!string.IsNullOrEmpty(_Value)) + return Convert.ToInt32(_Value); + else + return 0; + } + } + } + + public class VolumeMute : ValueProperty + { + public bool BoolValue { get; private set; } + + public string Value + { + set + { + // If the incoming value is "On" it sets the BoolValue true, otherwise sets it false + BoolValue = value == "On"; + OnValueChanged(); + } + } + } + + public class Audio + { + public Input Input { get; set; } + public Microphones Microphones { get; set; } // Can we have this setter fire the update on the CiscoCodec feedback? + public Output Output { get; set; } + public Volume Volume { get; set; } + public VolumeMute VolumeMute { get; set; } + + public Audio() + { + Volume = new Volume(); + VolumeMute = new VolumeMute(); + Microphones = new Microphones(); + } + } + + public class Id + { + public string Value { get; set; } + } + + public class Current + { + public Id Id { get; set; } + } + + public class Bookings + { + public Current Current { get; set; } + } + + public class Options + { + public string Value { get; set; } + } + + public class Capabilities + { + public Options Options { get; set; } + } + + public class Connected + { + public string Value { get; set; } + } + + public class Framerate + { + public string Value { get; set; } + } + + public class Flip + { + public string Value { get; set; } + } + + public class HardwareID + { + public string Value { get; set; } + } + + public class Manufacturer + { + public string Value { get; set; } + } + + public class Model + { + public string Value { get; set; } + } + + public class Pan + { + public string Value { get; set; } + } + + public class Tilt + { + public string Value { get; set; } + } + + public class Zoom + { + public string Value { get; set; } + } + + public class Position + { + public Pan Pan { get; set; } + public Tilt Tilt { get; set; } + public Zoom Zoom { get; set; } + } + + public class SoftwareID + { + public string Value { get; set; } + } + + public class Camera + { + public string id { get; set; } + public Capabilities Capabilities { get; set; } + public Connected Connected { get; set; } + public Flip Flip { get; set; } + public HardwareID HardwareID { get; set; } + public MacAddress MacAddress { get; set; } + public Manufacturer Manufacturer { get; set; } + public Model Model { get; set; } + public Position Position { get; set; } + public SerialNumber SerialNumber { get; set; } + public SoftwareID SoftwareID { get; set; } + } + + public class Availability + { + public string Value { get; set; } + } + + public class Status2 : ValueProperty + { + string _Value; + public bool BoolValue { get; private set; } + + public string Value + { + get + { + return _Value; + } + set + { + // If the incoming value is "Active" it sets the BoolValue true, otherwise sets it false + _Value = value; + BoolValue = value == "Active"; + OnValueChanged(); + } + } + } + + public class SpeakerTrack + { + public Availability Availability { get; set; } + public Status2 Status { get; set; } + + public SpeakerTrack() + { + Status = new Status2(); + } + } + + public class Cameras + { + // [JsonConverter(typeof(CameraConverter))] + public List Camera { get; set; } + public SpeakerTrack SpeakerTrack { get; set; } + + public Cameras() + { + Camera = new List(); + SpeakerTrack = new SpeakerTrack(); + } + } + + //public class CameraConverter : JsonConverter + //{ + + // public override bool CanConvert(System.Type objectType) + // { + // return true; // objectType == typeof(Camera) || objectType == typeof(List); // This should not be called but is required for implmentation + // } + + // public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer) + // { + // try + // { + // if (reader.TokenType == JsonToken.StartArray) + // { + // var l = new List(); + // reader.Read(); + // while (reader.TokenType != JsonToken.EndArray) + // { + // l.Add(reader.Value as Camera); + // reader.Read(); + // } + // Debug.Console(1, "[xStatus]: Cameras converted as list"); + // return l; + // } + // else + // { + // Debug.Console(1, "[xStatus]: Camera converted as single object and added to list"); + // return new List { reader.Value as Camera }; + // } + // } + // catch (Exception e) + // { + // Debug.Console(1, "[xStatus]: Unable to convert JSON for camera objects: {0}", e); + + // return new List(); + // } + // } + + // public override bool CanWrite + // { + // get + // { + // return false; + // } + // } + + // public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + // { + // throw new NotImplementedException("Write not implemented"); + // } + //} + + + public class MaxActiveCalls + { + public string Value { get; set; } + } + + public class MaxAudioCalls + { + public string Value { get; set; } + } + + public class MaxCalls + { + public string Value { get; set; } + } + + public class MaxVideoCalls + { + public string Value { get; set; } + } + + public class Conference + { + public MaxActiveCalls MaxActiveCalls { get; set; } + public MaxAudioCalls MaxAudioCalls { get; set; } + public MaxCalls MaxCalls { get; set; } + public MaxVideoCalls MaxVideoCalls { get; set; } + } + + public class Capabilities2 + { + public Conference Conference { get; set; } + } + + public class CallId + { + public string Value { get; set; } + } + + public class ActiveSpeaker + { + public CallId CallId { get; set; } + } + + public class DoNotDisturb + { + public string Value { get; set; } + } + + public class Mode + { + public string Value { get; set; } + } + + public class Multipoint + { + public Mode Mode { get; set; } + } + + public class CallId2 + { + public string Value { get; set; } + } + + public class Mode2 : ValueProperty + { + string _Value; + + public bool BoolValue { get; private set; } + + + + public string Value + { + get + { + return _Value; + } + set + { + _Value = value; + // If the incoming value is "Sending" it sets the BoolValue true, otherwise sets it false + BoolValue = value == "Sending"; + OnValueChanged(); + } + } + } + + public class Mode3 + { + public string Value { get; set; } + } + + public class ReleaseFloorAvailability + { + public string Value { get; set; } + } + + public class RequestFloorAvailability + { + public string Value { get; set; } + } + + public class Whiteboard + { + public Mode3 Mode { get; set; } + public ReleaseFloorAvailability ReleaseFloorAvailability { get; set; } + public RequestFloorAvailability RequestFloorAvailability { get; set; } + } + + public class Source2 : ValueProperty + { + string _Value; + + /// + /// Sets Value and triggers the action when set + /// + public string Value + { + get + { + return _Value; + } + set + { + _Value = value; + OnValueChanged(); + } + } + + /// + /// Converted value of _Value for use as feedback + /// + public int IntValue + { + get + { + if (!string.IsNullOrEmpty(_Value)) + return Convert.ToInt32(_Value); + else + return 0; + } + } + } + + public class SendingMode + { + public string Value { get; set; } + } + + public class LocalInstance + { + public string id { get; set; } + public string ghost { get; set; } + public SendingMode SendingMode { get; set; } + public Source2 Source { get; set; } + + public LocalInstance() + { + Source = new Source2(); + } + } + + public class Presentation + { + public CallId2 CallId { get; set; } + public Mode2 Mode { get; set; } + public Whiteboard Whiteboard { get; set; } + public List LocalInstance { get; set; } + + public Presentation() + { + Mode = new Mode2(); + LocalInstance = new List(); + } + } + + public class CallId3 + { + public string Value { get; set; } + } + + public class Mode4 + { + public string Value { get; set; } + } + + public class SpeakerLock + { + public CallId3 CallId { get; set; } + public Mode4 Mode { get; set; } + } + + public class Conference2 + { + public ActiveSpeaker ActiveSpeaker { get; set; } + public DoNotDisturb DoNotDisturb { get; set; } + public Multipoint Multipoint { get; set; } + public Presentation Presentation { get; set; } + public SpeakerLock SpeakerLock { get; set; } + + public Conference2() + { + Presentation = new Presentation(); + } + } + + public class Description + { + public string Value { get; set; } + } + + public class Level + { + public string Value { get; set; } + } + + public class References + { + public string Value { get; set; } + } + + public class Type + { + public string Value { get; set; } + } + + public class Message + { + public string id { get; set; } + public Description Description { get; set; } + public Level Level { get; set; } + public References References { get; set; } + public Type Type { get; set; } + } + + public class Diagnostics + { + public List Message { get; set; } + } + + public class Conference3 + { + } + + public class Experimental + { + public Conference3 Conference { get; set; } + } + + public class Address + { + public string Value { get; set; } + } + + public class Port + { + public string Value { get; set; } + } + + public class Reason + { + public string Value { get; set; } + } + + public class Status3 + { + public string Value { get; set; } + } + + public class Gatekeeper + { + public Address Address { get; set; } + public Port Port { get; set; } + public Reason Reason { get; set; } + public Status3 Status { get; set; } + } + + public class Reason2 + { + public string Value { get; set; } + } + + public class Status4 + { + public string Value { get; set; } + } + + public class Mode5 + { + public Reason2 Reason { get; set; } + public Status4 Status { get; set; } + } + + public class H323 + { + public Gatekeeper Gatekeeper { get; set; } + public Mode5 Mode { get; set; } + } + + public class Expression + { + public string id { get; set; } + public string Value { get; set; } + } + + public class Format + { + public string Value { get; set; } + } + + public class URL + { + public string Value { get; set; } + } + + public class HttpFeedback + { + public string id { get; set; } + public List Expression { get; set; } + public Format Format { get; set; } + public URL URL { get; set; } + } + + public class MediaChannels + { + } + + public class Address2 + { + public string Value { get; set; } + } + + public class Capabilities3 + { + public string Value { get; set; } + } + + public class DeviceId + { + public string Value { get; set; } + } + + public class Duplex + { + public string Value { get; set; } + } + + public class Platform + { + public string Value { get; set; } + } + + public class PortID + { + public string Value { get; set; } + } + + public class PrimaryMgmtAddress + { + public string Value { get; set; } + } + + public class SysName + { + public string Value { get; set; } + } + + public class SysObjectID + { + public string Value { get; set; } + } + + public class VTPMgmtDomain + { + public string Value { get; set; } + } + + public class Version + { + public string Value { get; set; } + } + + public class VoIPApplianceVlanID + { + public string Value { get; set; } + } + + public class CDP + { + public Address2 Address { get; set; } + public Capabilities3 Capabilities { get; set; } + public DeviceId DeviceId { get; set; } + public Duplex Duplex { get; set; } + public Platform Platform { get; set; } + public PortID PortID { get; set; } + public PrimaryMgmtAddress PrimaryMgmtAddress { get; set; } + public SysName SysName { get; set; } + public SysObjectID SysObjectID { get; set; } + public VTPMgmtDomain VTPMgmtDomain { get; set; } + public Version Version { get; set; } + public VoIPApplianceVlanID VoIPApplianceVlanID { get; set; } + } + + public class Name + { + public string Value { get; set; } + } + + public class Domain + { + public Name Name { get; set; } + } + + public class Address3 + { + public string Value { get; set; } + } + + public class Server + { + public string id { get; set; } + public Address3 Address { get; set; } + } + + public class DNS + { + public Domain Domain { get; set; } + public List Server { get; set; } + } + + public class MacAddress + { + public string Value { get; set; } + } + + public class Speed + { + public string Value { get; set; } + } + + public class Ethernet + { + public MacAddress MacAddress { get; set; } + public Speed Speed { get; set; } + } + + public class Address4 + { + public string Value { get; set; } + } + + public class Gateway + { + public string Value { get; set; } + } + + public class SubnetMask + { + public string Value { get; set; } + } + + public class IPv4 + { + public Address4 Address { get; set; } + public Gateway Gateway { get; set; } + public SubnetMask SubnetMask { get; set; } + } + + public class Address5 + { + public string Value { get; set; } + } + + public class Gateway2 + { + public string Value { get; set; } + } + + public class IPv6 + { + public Address5 Address { get; set; } + public Gateway2 Gateway { get; set; } + } + + public class VlanId + { + public string Value { get; set; } + } + + public class Voice + { + public VlanId VlanId { get; set; } + } + + public class VLAN + { + public Voice Voice { get; set; } + } + + public class Network + { + public string id { get; set; } + public CDP CDP { get; set; } + public DNS DNS { get; set; } + public Ethernet Ethernet { get; set; } + public IPv4 IPv4 { get; set; } + public IPv6 IPv6 { get; set; } + public VLAN VLAN { get; set; } + } + + public class CurrentAddress + { + public string Value { get; set; } + } + + public class Address6 + { + public string Value { get; set; } + } + + public class Server2 + { + public string id { get; set; } + public Address6 Address { get; set; } + } + + public class Status5 + { + public string Value { get; set; } + } + + public class NTP + { + public CurrentAddress CurrentAddress { get; set; } + public List Server { get; set; } + public Status5 Status { get; set; } + } + + public class NetworkServices + { + public NTP NTP { get; set; } + } + + public class HardwareInfo + { + public string Value { get; set; } + } + + public class ID2 + { + public string Value { get; set; } + } + + public class Name2 + { + public string Value { get; set; } + } + + public class SoftwareInfo + { + public string Value { get; set; } + } + + public class Status6 + { + public string Value { get; set; } + } + + public class Type2 + { + public string Value { get; set; } + } + + public class UpgradeStatus + { + public string Value { get; set; } + } + + public class ConnectedDevice + { + public string id { get; set; } + public HardwareInfo HardwareInfo { get; set; } + public ID2 ID { get; set; } + public Name2 Name { get; set; } + public SoftwareInfo SoftwareInfo { get; set; } + public Status6 Status { get; set; } + public Type2 Type { get; set; } + public UpgradeStatus UpgradeStatus { get; set; } + } + + public class Peripherals + { + public List ConnectedDevice { get; set; } + } + + public class Enabled + { + public string Value { get; set; } + } + + public class LastLoggedInUserId + { + public string Value { get; set; } + } + + public class LoggedIn + { + public string Value { get; set; } + } + + public class ExtensionMobility + { + public Enabled Enabled { get; set; } + public LastLoggedInUserId LastLoggedInUserId { get; set; } + public LoggedIn LoggedIn { get; set; } + } + + public class CUCM + { + public ExtensionMobility ExtensionMobility { get; set; } + } + + public class CompletedAt + { + public string Value { get; set; } + } + + public class URL2 + { + public string Value { get; set; } + } + + public class VersionId + { + public string Value { get; set; } + } + + public class Current2 + { + public CompletedAt CompletedAt { get; set; } + public URL2 URL { get; set; } + public VersionId VersionId { get; set; } + } + + public class LastChange + { + public string Value { get; set; } + } + + public class Message2 + { + public string Value { get; set; } + } + + public class Phase + { + public string Value { get; set; } + } + + public class SessionId + { + public string Value { get; set; } + } + + public class Status7 + { + public string Value { get; set; } + } + + public class URL3 + { + public string Value { get; set; } + } + + public class VersionId2 + { + public string Value { get; set; } + } + + public class UpgradeStatus2 + { + public LastChange LastChange { get; set; } + public Message2 Message { get; set; } + public Phase Phase { get; set; } + public SessionId SessionId { get; set; } + public Status7 Status { get; set; } + public URL3 URL { get; set; } + public VersionId2 VersionId { get; set; } + } + + public class Software + { + public Current2 Current { get; set; } + public UpgradeStatus2 UpgradeStatus { get; set; } + } + + public class Status8 + { + public string Value { get; set; } + } + + public class Provisioning + { + public CUCM CUCM { get; set; } + public Software Software { get; set; } + public Status8 Status { get; set; } + } + + public class Availability2 + { + public string Value { get; set; } + } + + public class Services + { + public Availability2 Availability { get; set; } + } + + public class Proximity + { + public Services Services { get; set; } + } + + public class Current3 : ValueProperty + { + string _Value; + + /// + /// Sets Value and triggers the action when set + /// + public string Value + { + get + { + return _Value; + } + set + { + _Value = value; + OnValueChanged(); + } + } + + /// + /// Converted value of _Value for use as feedback + /// + public int IntValue + { + get + { + if (!string.IsNullOrEmpty(_Value)) + return Convert.ToInt32(_Value); + else + return 0; + } + } + } + + public class PeopleCount + { + public Current3 Current { get; set; } + + public PeopleCount() + { + Current = new Current3(); + } + } + + public class PeoplePresence : ValueProperty + { + public bool BoolValue { get; private set; } + + public string Value + { + set + { + // If the incoming value is "Yes" it sets the BoolValue true, otherwise sets it false + BoolValue = value == "Yes"; + OnValueChanged(); + } + } + } + + public class RoomAnalytics + { + public PeopleCount PeopleCount { get; set; } + public PeoplePresence PeoplePresence { get; set; } + + public RoomAnalytics() + { + PeopleCount = new PeopleCount(); + PeoplePresence = new PeoplePresence(); + } + } + + public class Primary + { + public URI URI { get; set; } + + public Primary() + { + URI = new URI(); + } + } + + public class AlternateURI + { + public Primary Primary { get; set; } + + public AlternateURI() + { + Primary = new Primary(); + } + } + + public class Authentication + { + public string Value { get; set; } + } + + public class DisplayName + { + public string Value { get; set; } + } + + public class Mode6 + { + public string Value { get; set; } + } + + public class URI + { + public string Value { get; set; } + } + + public class CallForward + { + public DisplayName DisplayName { get; set; } + public Mode6 Mode { get; set; } + public URI URI { get; set; } + } + + public class MessagesWaiting + { + public string Value { get; set; } + } + + public class URI2 + { + public string Value { get; set; } + } + + public class Mailbox + { + public MessagesWaiting MessagesWaiting { get; set; } + public URI2 URI { get; set; } + } + + public class Address7 + { + public string Value { get; set; } + } + + public class Status9 + { + public string Value { get; set; } + } + + public class Proxy + { + public string id { get; set; } + public Address7 Address { get; set; } + public Status9 Status { get; set; } + } + + public class Reason3 + { + public string Value { get; set; } + } + + public class Status10 + { + public string Value { get; set; } + } + + public class URI3 + { + public string Value { get; set; } + } + + public class Registration + { + public string id { get; set; } + public Reason3 Reason { get; set; } + public Status10 Status { get; set; } + public URI3 URI { get; set; } + + public Registration() + { + URI = new URI3(); + } + } + + public class Secure + { + public string Value { get; set; } + } + + public class Verified + { + public string Value { get; set; } + } + + public class SIP + { + public AlternateURI AlternateURI { get; set; } + public Authentication Authentication { get; set; } + public CallForward CallForward { get; set; } + public Mailbox Mailbox { get; set; } + public List Proxy { get; set; } + public List Registration { get; set; } + public Secure Secure { get; set; } + public Verified Verified { get; set; } + + public SIP() + { + AlternateURI = new AlternateURI(); + Registration = new List(); + } + } + + public class Mode7 + { + public string Value { get; set; } + } + + public class FIPS + { + public Mode7 Mode { get; set; } + } + + public class CallHistory + { + public string Value { get; set; } + } + + public class Configurations + { + public string Value { get; set; } + } + + public class DHCP + { + public string Value { get; set; } + } + + public class InternalLogging + { + public string Value { get; set; } + } + + public class LocalPhonebook + { + public string Value { get; set; } + } + + public class Persistency + { + public CallHistory CallHistory { get; set; } + public Configurations Configurations { get; set; } + public DHCP DHCP { get; set; } + public InternalLogging InternalLogging { get; set; } + public LocalPhonebook LocalPhonebook { get; set; } + } + + public class Security + { + public FIPS FIPS { get; set; } + public Persistency Persistency { get; set; } + } + + public class State : ValueProperty + { + public bool BoolValue { get; private set; } + + public string Value // Valid values are Standby/EnteringStandby/Halfwake/Off + { + set + { + // If the incoming value is "Of" it sets the BoolValue true, otherwise sets it false + BoolValue = value == "Off"; + OnValueChanged(); + } + } + } + + public class Standby + { + public State State { get; set; } + + public Standby() + { + State = new State(); + } + } + + public class CompatibilityLevel + { + public string Value { get; set; } + } + + public class SerialNumber + { + public string Value { get; set; } + } + + public class Module + { + public CompatibilityLevel CompatibilityLevel { get; set; } + public SerialNumber SerialNumber { get; set; } + } + + public class Hardware + { + public Module Module { get; set; } + } + + public class ProductId + { + public string Value { get; set; } + } + + public class ProductPlatform + { + public string Value { get; set; } + } + + public class ProductType + { + public string Value { get; set; } + } + + public class DisplayName2 + { + public string Value { get; set; } + } + + public class Name3 + { + public string Value { get; set; } + } + + public class Encryption + { + public string Value { get; set; } + } + + public class MultiSite + { + public string Value { get; set; } + } + + public class RemoteMonitoring + { + public string Value { get; set; } + } + + public class OptionKeys + { + public Encryption Encryption { get; set; } + public MultiSite MultiSite { get; set; } + public RemoteMonitoring RemoteMonitoring { get; set; } + + public OptionKeys() + { + MultiSite = new MultiSite(); + } + } + + public class ReleaseDate + { + public string Value { get; set; } + } + + public class Version2 + { + public string Value { get; set; } + } + + public class Software2 + { + public DisplayName2 DisplayName { get; set; } + public Name3 Name { get; set; } + public OptionKeys OptionKeys { get; set; } + public ReleaseDate ReleaseDate { get; set; } + public Version2 Version { get; set; } + + public Software2() + { + OptionKeys = new OptionKeys(); + } + } + + public class NumberOfActiveCalls + { + public string Value { get; set; } + } + + public class NumberOfInProgressCalls + { + public string Value { get; set; } + } + + public class NumberOfSuspendedCalls + { + public string Value { get; set; } + } + + public class State2 + { + public NumberOfActiveCalls NumberOfActiveCalls { get; set; } + public NumberOfInProgressCalls NumberOfInProgressCalls { get; set; } + public NumberOfSuspendedCalls NumberOfSuspendedCalls { get; set; } + } + + public class Uptime + { + public string Value { get; set; } + } + + public class SystemUnit + { + public Hardware Hardware { get; set; } + public ProductId ProductId { get; set; } + public ProductPlatform ProductPlatform { get; set; } + public ProductType ProductType { get; set; } + public Software2 Software { get; set; } + public State2 State { get; set; } + public Uptime Uptime { get; set; } + + public SystemUnit() + { + Software = new Software2(); + } + } + + public class SystemTime + { + public DateTime Value { get; set; } + } + + public class Time + { + public SystemTime SystemTime { get; set; } + } + + public class Number + { + public string Value { get; set; } + } + + public class ContactMethod + { + public string id { get; set; } + public Number Number { get; set; } + } + + public class Name4 + { + public string Value { get; set; } + } + + public class ContactInfo + { + public List ContactMethod { get; set; } + public Name4 Name { get; set; } + } + + public class UserInterface + { + public ContactInfo ContactInfo { get; set; } + } + + public class PIPPosition + { + public string Value { get; set; } + } + + public class ActiveSpeaker2 + { + public PIPPosition PIPPosition { get; set; } + } + + public class Connected2 + { + public string Value { get; set; } + } + + public class SignalState + { + public string Value { get; set; } + } + + public class SourceId + { + public string Value { get; set; } + } + + public class Type3 + { + public string Value { get; set; } + } + + public class Connector + { + public string id { get; set; } + public Connected2 Connected { get; set; } + public SignalState SignalState { get; set; } + public SourceId SourceId { get; set; } + public Type3 Type { get; set; } + } + + public class MainVideoSource + { + public string Value { get; set; } + } + + public class ConnectorId + { + public string Value { get; set; } + } + + public class FormatStatus + { + public string Value { get; set; } + } + + public class FormatType + { + public string Value { get; set; } + } + + public class MediaChannelId + { + public string Value { get; set; } + } + + public class Height + { + public string Value { get; set; } + } + + public class RefreshRate + { + public string Value { get; set; } + } + + public class Width + { + public string Value { get; set; } + } + + public class Resolution + { + public Height Height { get; set; } + public RefreshRate RefreshRate { get; set; } + public Width Width { get; set; } + } + + public class Source + { + public string id { get; set; } + public ConnectorId ConnectorId { get; set; } + public FormatStatus FormatStatus { get; set; } + public FormatType FormatType { get; set; } + public MediaChannelId MediaChannelId { get; set; } + public Resolution Resolution { get; set; } + } + + public class Input2 + { + public List Connector { get; set; } + public MainVideoSource MainVideoSource { get; set; } + public List Source { get; set; } + } + + public class Local : ValueProperty + { + string _Value; + + public string Value // Valid values are On/Off + { + get + { + return _Value; + } + set + { + _Value = value; + OnValueChanged(); + } + } + } + + public class LayoutFamily + { + public Local Local { get; set; } + + public LayoutFamily() + { + Local = new Local(); + } + } + + public class Layout + { + public LayoutFamily LayoutFamily { get; set; } + + public Layout() + { + LayoutFamily = new LayoutFamily(); + } + } + + public class Monitors + { + public string Value { get; set; } + } + + public class Connected3 + { + public string Value { get; set; } + } + + public class Name5 + { + public string Value { get; set; } + } + + public class PreferredFormat + { + public string Value { get; set; } + } + + public class ConnectedDevice2 + { + public Name5 Name { get; set; } + public PreferredFormat PreferredFormat { get; set; } + } + + public class MonitorRole + { + public string Value { get; set; } + } + + public class Height2 + { + public string Value { get; set; } + } + + public class RefreshRate2 + { + public string Value { get; set; } + } + + public class Width2 + { + public string Value { get; set; } + } + + public class Resolution2 + { + public Height2 Height { get; set; } + public RefreshRate2 RefreshRate { get; set; } + public Width2 Width { get; set; } + } + + public class Type4 + { + public string Value { get; set; } + } + + public class Connector2 + { + public string id { get; set; } + public Connected3 Connected { get; set; } + public ConnectedDevice2 ConnectedDevice { get; set; } + public MonitorRole MonitorRole { get; set; } + public Resolution2 Resolution { get; set; } + public Type4 Type { get; set; } + } + + public class Output2 + { + public List Connector { get; set; } + } + + public class PIPPosition2 + { + public string Value { get; set; } + } + + public class Presentation2 + { + public PIPPosition2 PIPPosition { get; set; } + } + + public class FullscreenMode + { + public string Value { get; set; } + } + + public class Mode8 : ValueProperty + { + public bool BoolValue { get; private set; } + + public string Value // Valid values are On/Off + { + set + { + // If the incoming value is "On" it sets the BoolValue true, otherwise sets it false + BoolValue = value == "On"; + OnValueChanged(); + } + } + } + + + public class OnMonitorRole + { + public string Value { get; set; } + } + + public class PIPPosition3 : ValueProperty + { + string _Value; + + public string Value + { + get + { + return _Value; + } + set + { + _Value = value; + OnValueChanged(); + } + } + } + + public class Selfview + { + public FullscreenMode FullscreenMode { get; set; } + public Mode8 Mode { get; set; } + public OnMonitorRole OnMonitorRole { get; set; } + public PIPPosition3 PIPPosition { get; set; } + + public Selfview() + { + Mode = new Mode8(); + PIPPosition = new PIPPosition3(); + } + } + + public class Video + { + public ActiveSpeaker2 ActiveSpeaker { get; set; } + public Input2 Input { get; set; } + public Layout Layout { get; set; } + public Monitors Monitors { get; set; } + public Output2 Output { get; set; } + public Presentation2 Presentation { get; set; } + public Selfview Selfview { get; set; } + + public Video() + { + Selfview = new Selfview(); + Layout = new Layout(); + } + } + + public class AnswerState + { + public string Value { get; set; } + } + + public class CallType + { + public string Value { get; set; } + } + + public class CallbackNumber + { + public string Value { get; set; } + } + + public class DeviceType + { + public string Value { get; set; } + } + + public class Direction + { + public string Value { get; set; } + } + + public class Duration + { + public string Value { get; set; } + } + + public class FacilityServiceId + { + public string Value { get; set; } + } + + public class HoldReason + { + public string Value { get; set; } + } + + public class PlacedOnHold + { + public string Value { get; set; } + } + + public class Protocol + { + public string Value { get; set; } + } + + public class ReceiveCallRate + { + public string Value { get; set; } + } + + public class RemoteNumber + { + public string Value { get; set; } + } + + public class TransmitCallRate + { + public string Value { get; set; } + } + + public class Call + { + public string id { get; set; } + public AnswerState AnswerState { get; set; } + public CallType CallType { get; set; } + public CallbackNumber CallbackNumber { get; set; } + public DeviceType DeviceType { get; set; } + public Direction Direction { get; set; } + public DisplayName DisplayName { get; set; } + public Duration Duration { get; set; } + public Encryption Encryption { get; set; } + public FacilityServiceId FacilityServiceId { get; set; } + public string ghost { get; set; } + public HoldReason HoldReason { get; set; } + public PlacedOnHold PlacedOnHold { get; set; } + public Protocol Protocol { get; set; } + public ReceiveCallRate ReceiveCallRate { get; set; } + public RemoteNumber RemoteNumber { get; set; } + public Status2 Status { get; set; } + public TransmitCallRate TransmitCallRate { get; set; } + + public Call() + { + CallType = new CallType(); + Status = new Status2(); + } + } + + public class Type5 + { + public string Value { get; set; } + } + + public class Description2 : ValueProperty + { + string _Value; + + public string Value + { + get + { + return _Value; + } + set + { + _Value = value; + OnValueChanged(); + } + } + } + + public class Defined : ValueProperty + { + public bool BoolValue { get; private set; } + + public string Value // Valid values are True/False + { + set + { + // If the incoming value is "True" it sets the BoolValue true, otherwise sets it false + BoolValue = value == "True"; + OnValueChanged(); + } + } + } + + public class RoomPreset + { + public string id { get; set; } + public Defined Defined { get; set; } + public Description2 Description { get; set; } + public Type5 Type { get; set; } + + public RoomPreset() + { + Defined = new Defined(); + Description = new Description2(); + Type = new Type5(); + } + } + + + + public class Status + { + public Audio Audio { get; set; } + public Bookings Bookings { get; set; } + public List Call { get; set; } + public Cameras Cameras { get; set; } + public Capabilities2 Capabilities { get; set; } + public Conference2 Conference { get; set; } + public Diagnostics Diagnostics { get; set; } + public Experimental Experimental { get; set; } + public H323 H323 { get; set; } + public List HttpFeedback { get; set; } + public MediaChannels MediaChannels { get; set; } + public List Network { get; set; } + public NetworkServices NetworkServices { get; set; } + public Peripherals Peripherals { get; set; } + public Provisioning Provisioning { get; set; } + public Proximity Proximity { get; set; } + public RoomAnalytics RoomAnalytics { get; set; } + + public List RoomPreset { get; set; } + + public SIP SIP { get; set; } + public Security Security { get; set; } + public Standby Standby { get; set; } + public SystemUnit SystemUnit { get; set; } + public Time Time { get; set; } + public UserInterface UserInterface { get; set; } + public Video Video { get; set; } + + public Status() + { + Audio = new Audio(); + Call = new List(); + Standby = new Standby(); + Cameras = new Cameras(); + RoomAnalytics = new RoomAnalytics(); + RoomPreset = new List(); + Conference = new Conference2(); + SystemUnit = new SystemUnit(); + Video = new Video(); + } + } + + public class RootObject + { + public Status Status { get; set; } + + public RootObject() + { + Status = new Status(); + } + } + } +} diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xStatusSparkPlus.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xStatusSparkPlus.cs new file mode 100644 index 00000000..9f7bf0b6 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/xStatusSparkPlus.cs @@ -0,0 +1,1552 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.CiscoCodec +{ + public class xStatusSparkPlus + { + public class ConnectionStatus + { + public string Value { get; set; } + } + + public class EcReferenceDelay + { + public string Value { get; set; } + } + + public class Microphone + { + public string id { get; set; } + public ConnectionStatus ConnectionStatus { get; set; } + public EcReferenceDelay EcReferenceDelay { get; set; } + } + + public class Connectors + { + public List Microphone { get; set; } + } + + public class Input + { + public Connectors Connectors { get; set; } + } + + public class Mute + { + public string Value { get; set; } + } + + public class Microphones + { + public Mute Mute { get; set; } + } + + public class ConnectionStatus2 + { + public string Value { get; set; } + } + + public class DelayMs + { + public string Value { get; set; } + } + + public class Line + { + public string id { get; set; } + public ConnectionStatus2 ConnectionStatus { get; set; } + public DelayMs DelayMs { get; set; } + } + + public class Connectors2 + { + public List Line { get; set; } + } + + public class Output + { + public Connectors2 Connectors { get; set; } + } + + public class Volume + { + public string Value { get; set; } + } + + public class VolumeMute + { + public string Value { get; set; } + } + + public class Audio + { + public Input Input { get; set; } + public Microphones Microphones { get; set; } + public Output Output { get; set; } + public Volume Volume { get; set; } + public VolumeMute VolumeMute { get; set; } + } + + public class Id + { + public string Value { get; set; } + } + + public class Current + { + public Id Id { get; set; } + } + + public class Bookings + { + public Current Current { get; set; } + } + + public class Options + { + public string Value { get; set; } + } + + public class Capabilities + { + public Options Options { get; set; } + } + + public class Connected + { + public string Value { get; set; } + } + + public class Flip + { + public string Value { get; set; } + } + + public class HardwareID + { + public string Value { get; set; } + } + + public class MacAddress + { + public string Value { get; set; } + } + + public class Manufacturer + { + public string Value { get; set; } + } + + public class Model + { + public string Value { get; set; } + } + + public class Pan + { + public string Value { get; set; } + } + + public class Tilt + { + public string Value { get; set; } + } + + public class Zoom + { + public string Value { get; set; } + } + + public class Position + { + public Pan Pan { get; set; } + public Tilt Tilt { get; set; } + public Zoom Zoom { get; set; } + } + + public class SerialNumber + { + public string Value { get; set; } + } + + public class SoftwareID + { + public string Value { get; set; } + } + + public class Camera + { + public string id { get; set; } + public Capabilities Capabilities { get; set; } + public Connected Connected { get; set; } + public Flip Flip { get; set; } + public HardwareID HardwareID { get; set; } + public MacAddress MacAddress { get; set; } + public Manufacturer Manufacturer { get; set; } + public Model Model { get; set; } + public Position Position { get; set; } + public SerialNumber SerialNumber { get; set; } + public SoftwareID SoftwareID { get; set; } + } + + public class Availability + { + public string Value { get; set; } + } + + public class Status2 + { + public string Value { get; set; } + } + + public class SpeakerTrack + { + public Availability Availability { get; set; } + public Status2 Status { get; set; } + } + + public class Cameras + { + public List Camera { get; set; } + public SpeakerTrack SpeakerTrack { get; set; } + } + + public class MaxActiveCalls + { + public string Value { get; set; } + } + + public class MaxAudioCalls + { + public string Value { get; set; } + } + + public class MaxCalls + { + public string Value { get; set; } + } + + public class MaxVideoCalls + { + public string Value { get; set; } + } + + public class Conference + { + public MaxActiveCalls MaxActiveCalls { get; set; } + public MaxAudioCalls MaxAudioCalls { get; set; } + public MaxCalls MaxCalls { get; set; } + public MaxVideoCalls MaxVideoCalls { get; set; } + } + + public class Capabilities2 + { + public Conference Conference { get; set; } + } + + public class CallId + { + public string Value { get; set; } + } + + public class ActiveSpeaker + { + public CallId CallId { get; set; } + } + + public class DoNotDisturb + { + public string Value { get; set; } + } + + public class Mode + { + public string Value { get; set; } + } + + public class Line2 + { + public string id { get; set; } + public Mode Mode { get; set; } + } + + public class Mode2 + { + public string Value { get; set; } + } + + public class Multipoint + { + public Mode2 Mode { get; set; } + } + + public class CallId2 + { + public string Value { get; set; } + } + + public class SendingMode + { + public string Value { get; set; } + } + + public class Source + { + public string Value { get; set; } + } + + public class LocalInstance + { + public string id { get; set; } + public SendingMode SendingMode { get; set; } + public Source Source { get; set; } + } + + public class Mode3 + { + public string Value { get; set; } + } + + public class Mode4 + { + public string Value { get; set; } + } + + public class ReleaseFloorAvailability + { + public string Value { get; set; } + } + + public class RequestFloorAvailability + { + public string Value { get; set; } + } + + public class Whiteboard + { + public Mode4 Mode { get; set; } + public ReleaseFloorAvailability ReleaseFloorAvailability { get; set; } + public RequestFloorAvailability RequestFloorAvailability { get; set; } + } + + public class Presentation + { + public CallId2 CallId { get; set; } + public List LocalInstance { get; set; } + public Mode3 Mode { get; set; } + public Whiteboard Whiteboard { get; set; } + } + + public class CallId3 + { + public string Value { get; set; } + } + + public class Mode5 + { + public string Value { get; set; } + } + + public class SpeakerLock + { + public CallId3 CallId { get; set; } + public Mode5 Mode { get; set; } + } + + public class Conference2 + { + public ActiveSpeaker ActiveSpeaker { get; set; } + public DoNotDisturb DoNotDisturb { get; set; } + public List Line { get; set; } + public Multipoint Multipoint { get; set; } + public Presentation Presentation { get; set; } + public SpeakerLock SpeakerLock { get; set; } + } + + public class Conference3 + { + } + + public class Experimental + { + public Conference3 Conference { get; set; } + } + + public class Address + { + public string Value { get; set; } + } + + public class Port + { + public string Value { get; set; } + } + + public class Reason + { + public string Value { get; set; } + } + + public class Status3 + { + public string Value { get; set; } + } + + public class Gatekeeper + { + public Address Address { get; set; } + public Port Port { get; set; } + public Reason Reason { get; set; } + public Status3 Status { get; set; } + } + + public class Reason2 + { + public string Value { get; set; } + } + + public class Status4 + { + public string Value { get; set; } + } + + public class Mode6 + { + public Reason2 Reason { get; set; } + public Status4 Status { get; set; } + } + + public class H323 + { + public Gatekeeper Gatekeeper { get; set; } + public Mode6 Mode { get; set; } + } + + public class Expression + { + public string id { get; set; } + public string Value { get; set; } + } + + public class Format + { + public string Value { get; set; } + } + + public class URL + { + public string Value { get; set; } + } + + public class HttpFeedback + { + public string id { get; set; } + public List Expression { get; set; } + public Format Format { get; set; } + public URL URL { get; set; } + } + + public class MediaChannels + { + } + + public class Address2 + { + public string Value { get; set; } + } + + public class Capabilities3 + { + public string Value { get; set; } + } + + public class DeviceId + { + public string Value { get; set; } + } + + public class Duplex + { + public string Value { get; set; } + } + + public class Platform + { + public string Value { get; set; } + } + + public class PortID + { + public string Value { get; set; } + } + + public class PrimaryMgmtAddress + { + public string Value { get; set; } + } + + public class SysName + { + public string Value { get; set; } + } + + public class SysObjectID + { + public string Value { get; set; } + } + + public class VTPMgmtDomain + { + public string Value { get; set; } + } + + public class Version + { + public string Value { get; set; } + } + + public class VoIPApplianceVlanID + { + public string Value { get; set; } + } + + public class CDP + { + public Address2 Address { get; set; } + public Capabilities3 Capabilities { get; set; } + public DeviceId DeviceId { get; set; } + public Duplex Duplex { get; set; } + public Platform Platform { get; set; } + public PortID PortID { get; set; } + public PrimaryMgmtAddress PrimaryMgmtAddress { get; set; } + public SysName SysName { get; set; } + public SysObjectID SysObjectID { get; set; } + public VTPMgmtDomain VTPMgmtDomain { get; set; } + public Version Version { get; set; } + public VoIPApplianceVlanID VoIPApplianceVlanID { get; set; } + } + + public class Name + { + public string Value { get; set; } + } + + public class Domain + { + public Name Name { get; set; } + } + + public class Address3 + { + public string Value { get; set; } + } + + public class Server + { + public string id { get; set; } + public Address3 Address { get; set; } + } + + public class DNS + { + public Domain Domain { get; set; } + public List Server { get; set; } + } + + public class MacAddress2 + { + public string Value { get; set; } + } + + public class Speed + { + public string Value { get; set; } + } + + public class Ethernet + { + public MacAddress2 MacAddress { get; set; } + public Speed Speed { get; set; } + } + + public class Address4 + { + public string Value { get; set; } + } + + public class Gateway + { + public string Value { get; set; } + } + + public class SubnetMask + { + public string Value { get; set; } + } + + public class IPv4 + { + public Address4 Address { get; set; } + public Gateway Gateway { get; set; } + public SubnetMask SubnetMask { get; set; } + } + + public class Address5 + { + public string Value { get; set; } + } + + public class Gateway2 + { + public string Value { get; set; } + } + + public class IPv6 + { + public Address5 Address { get; set; } + public Gateway2 Gateway { get; set; } + } + + public class VlanId + { + public string Value { get; set; } + } + + public class Voice + { + public VlanId VlanId { get; set; } + } + + public class VLAN + { + public Voice Voice { get; set; } + } + + public class Network + { + public string id { get; set; } + public CDP CDP { get; set; } + public DNS DNS { get; set; } + public Ethernet Ethernet { get; set; } + public IPv4 IPv4 { get; set; } + public IPv6 IPv6 { get; set; } + public VLAN VLAN { get; set; } + } + + public class CurrentAddress + { + public string Value { get; set; } + } + + public class Address6 + { + public string Value { get; set; } + } + + public class Server2 + { + public string id { get; set; } + public Address6 Address { get; set; } + } + + public class Status5 + { + public string Value { get; set; } + } + + public class NTP + { + public CurrentAddress CurrentAddress { get; set; } + public List Server { get; set; } + public Status5 Status { get; set; } + } + + public class NetworkServices + { + public NTP NTP { get; set; } + } + + public class HardwareInfo + { + public string Value { get; set; } + } + + public class ID2 + { + public string Value { get; set; } + } + + public class Name2 + { + public string Value { get; set; } + } + + public class SoftwareInfo + { + public string Value { get; set; } + } + + public class Status6 + { + public string Value { get; set; } + } + + public class Type + { + public string Value { get; set; } + } + + public class UpgradeStatus + { + public string Value { get; set; } + } + + public class ConnectedDevice + { + public string id { get; set; } + public HardwareInfo HardwareInfo { get; set; } + public ID2 ID { get; set; } + public Name2 Name { get; set; } + public SoftwareInfo SoftwareInfo { get; set; } + public Status6 Status { get; set; } + public Type Type { get; set; } + public UpgradeStatus UpgradeStatus { get; set; } + } + + public class Peripherals + { + public List ConnectedDevice { get; set; } + } + + public class Enabled + { + public string Value { get; set; } + } + + public class LastLoggedInUserId + { + public string Value { get; set; } + } + + public class LoggedIn + { + public string Value { get; set; } + } + + public class ExtensionMobility + { + public Enabled Enabled { get; set; } + public LastLoggedInUserId LastLoggedInUserId { get; set; } + public LoggedIn LoggedIn { get; set; } + } + + public class CUCM + { + public ExtensionMobility ExtensionMobility { get; set; } + } + + public class CompletedAt + { + public string Value { get; set; } + } + + public class URL2 + { + public string Value { get; set; } + } + + public class VersionId + { + public string Value { get; set; } + } + + public class Current2 + { + public CompletedAt CompletedAt { get; set; } + public URL2 URL { get; set; } + public VersionId VersionId { get; set; } + } + + public class LastChange + { + public string Value { get; set; } + } + + public class Message + { + public string Value { get; set; } + } + + public class Phase + { + public string Value { get; set; } + } + + public class SessionId + { + public string Value { get; set; } + } + + public class Status7 + { + public string Value { get; set; } + } + + public class URL3 + { + public string Value { get; set; } + } + + public class VersionId2 + { + public string Value { get; set; } + } + + public class UpgradeStatus2 + { + public LastChange LastChange { get; set; } + public Message Message { get; set; } + public Phase Phase { get; set; } + public SessionId SessionId { get; set; } + public Status7 Status { get; set; } + public URL3 URL { get; set; } + public VersionId2 VersionId { get; set; } + } + + public class Software + { + public Current2 Current { get; set; } + public UpgradeStatus2 UpgradeStatus { get; set; } + } + + public class Status8 + { + public string Value { get; set; } + } + + public class Provisioning + { + public CUCM CUCM { get; set; } + public Software Software { get; set; } + public Status8 Status { get; set; } + } + + public class Availability2 + { + public string Value { get; set; } + } + + public class Services + { + public Availability2 Availability { get; set; } + } + + public class Proximity + { + public Services Services { get; set; } + } + + public class Current3 + { + public string Value { get; set; } + } + + public class PeopleCount + { + public Current3 Current { get; set; } + } + + public class PeoplePresence + { + public string Value { get; set; } + } + + public class RoomAnalytics + { + public PeopleCount PeopleCount { get; set; } + public PeoplePresence PeoplePresence { get; set; } + } + + public class URI + { + public string Value { get; set; } + } + + public class Primary + { + public URI URI { get; set; } + } + + public class AlternateURI + { + public Primary Primary { get; set; } + } + + public class Authentication + { + public string Value { get; set; } + } + + public class DisplayName + { + public string Value { get; set; } + } + + public class Mode7 + { + public string Value { get; set; } + } + + public class URI2 + { + public string Value { get; set; } + } + + public class CallForward + { + public DisplayName DisplayName { get; set; } + public Mode7 Mode { get; set; } + public URI2 URI { get; set; } + } + + public class MessagesWaiting + { + public string Value { get; set; } + } + + public class URI3 + { + public string Value { get; set; } + } + + public class Mailbox + { + public MessagesWaiting MessagesWaiting { get; set; } + public URI3 URI { get; set; } + } + + public class Address7 + { + public string Value { get; set; } + } + + public class Status9 + { + public string Value { get; set; } + } + + public class Proxy + { + public string id { get; set; } + public Address7 Address { get; set; } + public Status9 Status { get; set; } + } + + public class Reason3 + { + public string Value { get; set; } + } + + public class Status10 + { + public string Value { get; set; } + } + + public class URI4 + { + public string Value { get; set; } + } + + public class Registration + { + public string id { get; set; } + public Reason3 Reason { get; set; } + public Status10 Status { get; set; } + public URI4 URI { get; set; } + } + + public class Secure + { + public string Value { get; set; } + } + + public class Verified + { + public string Value { get; set; } + } + + public class SIP + { + public AlternateURI AlternateURI { get; set; } + public Authentication Authentication { get; set; } + public CallForward CallForward { get; set; } + public Mailbox Mailbox { get; set; } + public List Proxy { get; set; } + public List Registration { get; set; } + public Secure Secure { get; set; } + public Verified Verified { get; set; } + } + + public class Mode8 + { + public string Value { get; set; } + } + + public class FIPS + { + public Mode8 Mode { get; set; } + } + + public class CallHistory + { + public string Value { get; set; } + } + + public class Configurations + { + public string Value { get; set; } + } + + public class DHCP + { + public string Value { get; set; } + } + + public class InternalLogging + { + public string Value { get; set; } + } + + public class LocalPhonebook + { + public string Value { get; set; } + } + + public class Persistency + { + public CallHistory CallHistory { get; set; } + public Configurations Configurations { get; set; } + public DHCP DHCP { get; set; } + public InternalLogging InternalLogging { get; set; } + public LocalPhonebook LocalPhonebook { get; set; } + } + + public class Security + { + public FIPS FIPS { get; set; } + public Persistency Persistency { get; set; } + } + + public class State + { + public string Value { get; set; } + } + + public class Standby + { + public State State { get; set; } + } + + public class CompatibilityLevel + { + public string Value { get; set; } + } + + public class SerialNumber2 + { + public string Value { get; set; } + } + + public class Module + { + public CompatibilityLevel CompatibilityLevel { get; set; } + public SerialNumber2 SerialNumber { get; set; } + } + + public class Hardware + { + public Module Module { get; set; } + } + + public class ProductId + { + public string Value { get; set; } + } + + public class ProductPlatform + { + public string Value { get; set; } + } + + public class ProductType + { + public string Value { get; set; } + } + + public class DisplayName2 + { + public string Value { get; set; } + } + + public class Name3 + { + public string Value { get; set; } + } + + public class Encryption + { + public string Value { get; set; } + } + + public class MultiSite + { + public string Value { get; set; } + } + + public class RemoteMonitoring + { + public string Value { get; set; } + } + + public class OptionKeys + { + public Encryption Encryption { get; set; } + public MultiSite MultiSite { get; set; } + public RemoteMonitoring RemoteMonitoring { get; set; } + } + + public class ReleaseDate + { + public string Value { get; set; } + } + + public class Version2 + { + public string Value { get; set; } + } + + public class Software2 + { + public DisplayName2 DisplayName { get; set; } + public Name3 Name { get; set; } + public OptionKeys OptionKeys { get; set; } + public ReleaseDate ReleaseDate { get; set; } + public Version2 Version { get; set; } + } + + public class NumberOfActiveCalls + { + public string Value { get; set; } + } + + public class NumberOfInProgressCalls + { + public string Value { get; set; } + } + + public class NumberOfSuspendedCalls + { + public string Value { get; set; } + } + + public class State2 + { + public NumberOfActiveCalls NumberOfActiveCalls { get; set; } + public NumberOfInProgressCalls NumberOfInProgressCalls { get; set; } + public NumberOfSuspendedCalls NumberOfSuspendedCalls { get; set; } + } + + public class Uptime + { + public string Value { get; set; } + } + + public class SystemUnit + { + public Hardware Hardware { get; set; } + public ProductId ProductId { get; set; } + public ProductPlatform ProductPlatform { get; set; } + public ProductType ProductType { get; set; } + public Software2 Software { get; set; } + public State2 State { get; set; } + public Uptime Uptime { get; set; } + } + + public class SystemTime + { + public DateTime Value { get; set; } + } + + public class Time + { + public SystemTime SystemTime { get; set; } + } + + public class Number + { + public string Value { get; set; } + } + + public class ContactMethod + { + public string id { get; set; } + public Number Number { get; set; } + } + + public class Name4 + { + public string Value { get; set; } + } + + public class ContactInfo + { + public List ContactMethod { get; set; } + public Name4 Name { get; set; } + } + + public class UserInterface + { + public ContactInfo ContactInfo { get; set; } + } + + public class PIPPosition + { + public string Value { get; set; } + } + + public class ActiveSpeaker2 + { + public PIPPosition PIPPosition { get; set; } + } + + public class Connected2 + { + public string Value { get; set; } + } + + public class DeviceType + { + public string Value { get; set; } + } + + public class Name5 + { + public string Value { get; set; } + } + + public class PowerStatus + { + public string Value { get; set; } + } + + public class VendorId + { + public string Value { get; set; } + } + + public class CEC + { + public string id { get; set; } + public DeviceType DeviceType { get; set; } + public Name5 Name { get; set; } + public PowerStatus PowerStatus { get; set; } + public VendorId VendorId { get; set; } + } + + public class ConnectedDevice2 + { + public List CEC { get; set; } + } + + public class SignalState + { + public string Value { get; set; } + } + + public class SourceId + { + public string Value { get; set; } + } + + public class Type2 + { + public string Value { get; set; } + } + + public class Connector + { + public string id { get; set; } + public Connected2 Connected { get; set; } + public ConnectedDevice2 ConnectedDevice { get; set; } + public SignalState SignalState { get; set; } + public SourceId SourceId { get; set; } + public Type2 Type { get; set; } + } + + public class MainVideoSource + { + public string Value { get; set; } + } + + public class ConnectorId + { + public string Value { get; set; } + } + + public class FormatStatus + { + public string Value { get; set; } + } + + public class FormatType + { + public string Value { get; set; } + } + + public class MediaChannelId + { + public string Value { get; set; } + } + + public class Height + { + public string Value { get; set; } + } + + public class RefreshRate + { + public string Value { get; set; } + } + + public class Width + { + public string Value { get; set; } + } + + public class Resolution + { + public Height Height { get; set; } + public RefreshRate RefreshRate { get; set; } + public Width Width { get; set; } + } + + public class Source2 + { + public string id { get; set; } + public ConnectorId ConnectorId { get; set; } + public FormatStatus FormatStatus { get; set; } + public FormatType FormatType { get; set; } + public MediaChannelId MediaChannelId { get; set; } + public Resolution Resolution { get; set; } + } + + public class Input2 + { + public List Connector { get; set; } + public MainVideoSource MainVideoSource { get; set; } + public List Source { get; set; } + } + + public class Local + { + public string Value { get; set; } + } + + public class LayoutFamily + { + public Local Local { get; set; } + } + + public class Layout + { + public LayoutFamily LayoutFamily { get; set; } + } + + public class Monitors + { + public string Value { get; set; } + } + + public class Connected3 + { + public string Value { get; set; } + } + + public class DeviceType2 + { + public string Value { get; set; } + } + + public class Name6 + { + public string Value { get; set; } + } + + public class PowerStatus2 + { + public string Value { get; set; } + } + + public class VendorId2 + { + public string Value { get; set; } + } + + public class CEC2 + { + public string id { get; set; } + public DeviceType2 DeviceType { get; set; } + public Name6 Name { get; set; } + public PowerStatus2 PowerStatus { get; set; } + public VendorId2 VendorId { get; set; } + } + + public class Name7 + { + public string Value { get; set; } + } + + public class PreferredFormat + { + public string Value { get; set; } + } + + public class ConnectedDevice3 + { + public List CEC { get; set; } + public Name7 Name { get; set; } + public PreferredFormat PreferredFormat { get; set; } + } + + public class MonitorRole + { + public string Value { get; set; } + } + + public class Height2 + { + public string Value { get; set; } + } + + public class RefreshRate2 + { + public string Value { get; set; } + } + + public class Width2 + { + public string Value { get; set; } + } + + public class Resolution2 + { + public Height2 Height { get; set; } + public RefreshRate2 RefreshRate { get; set; } + public Width2 Width { get; set; } + } + + public class Type3 + { + public string Value { get; set; } + } + + public class Connector2 + { + public string id { get; set; } + public Connected3 Connected { get; set; } + public ConnectedDevice3 ConnectedDevice { get; set; } + public MonitorRole MonitorRole { get; set; } + public Resolution2 Resolution { get; set; } + public Type3 Type { get; set; } + } + + public class Output2 + { + public List Connector { get; set; } + } + + public class PIPPosition2 + { + public string Value { get; set; } + } + + public class Presentation2 + { + public PIPPosition2 PIPPosition { get; set; } + } + + public class FullscreenMode + { + public string Value { get; set; } + } + + public class Mode9 + { + public string Value { get; set; } + } + + public class OnMonitorRole + { + public string Value { get; set; } + } + + public class PIPPosition3 + { + public string Value { get; set; } + } + + public class Selfview + { + public FullscreenMode FullscreenMode { get; set; } + public Mode9 Mode { get; set; } + public OnMonitorRole OnMonitorRole { get; set; } + public PIPPosition3 PIPPosition { get; set; } + } + + public class Video + { + public ActiveSpeaker2 ActiveSpeaker { get; set; } + public Input2 Input { get; set; } + public Layout Layout { get; set; } + public Monitors Monitors { get; set; } + public Output2 Output { get; set; } + public Presentation2 Presentation { get; set; } + public Selfview Selfview { get; set; } + } + + public class Status + { + public Audio Audio { get; set; } + public Bookings Bookings { get; set; } + public Cameras Cameras { get; set; } + public Capabilities2 Capabilities { get; set; } + public Conference2 Conference { get; set; } + public Experimental Experimental { get; set; } + public H323 H323 { get; set; } + public List HttpFeedback { get; set; } + public MediaChannels MediaChannels { get; set; } + public List Network { get; set; } + public NetworkServices NetworkServices { get; set; } + public Peripherals Peripherals { get; set; } + public Provisioning Provisioning { get; set; } + public Proximity Proximity { get; set; } + public RoomAnalytics RoomAnalytics { get; set; } + public SIP SIP { get; set; } + public Security Security { get; set; } + public Standby Standby { get; set; } + public SystemUnit SystemUnit { get; set; } + public Time Time { get; set; } + public UserInterface UserInterface { get; set; } + public Video Video { get; set; } + } + + public class RootObject + { + public Status Status { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CodecActiveCallItem.cs.orig b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CodecActiveCallItem.cs.orig new file mode 100644 index 00000000..4b47d127 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CodecActiveCallItem.cs.orig @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec + +{ + public class CodecActiveCallItem + { + public string Name { get; set; } + + public string Number { get; set; } + +<<<<<<< HEAD + public eCodecCallType Type { get; private set; } + + public CodecActiveCallItem(string name, string number, eCodecCallType type) + { + Name = name; + Number = number; + Type = type; + } +======= + public eCodecCallType Type { get; set; } + + public eCodecCallStatus Status { get; set; } + + public string Id { get; set; } +>>>>>>> origin/feature/cisco-spark-2 + } + + public enum eCodecCallType + { + None, Audio, Video + } + + public enum eCodecCallStatus + { + Dialing, Established, Incoming + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/CameraControl.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/CameraControl.cs new file mode 100644 index 00000000..b17bd325 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/CameraControl.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.Cameras +{ + public enum eCameraControlMode + { + Off = 0, + Manual, + Auto + } + + + public interface IHasCameras + { + event EventHandler CameraSelected; + + List Cameras { get; } + + CameraBase SelectedCamera { get; } + + StringFeedback SelectedCameraFeedback { get; } + + void SelectCamera(string key); + } + + /// + /// Aggregates far end cameras with near end cameras + /// + public interface IHasCodecCameras : IHasCameras, IHasFarEndCameraControl + { + + } + + /// + /// To be implmented on codecs that can disable their camera(s) to blank the near end video + /// + public interface IHasCameraOff + { + BoolFeedback CameraIsOffFeedback { get; } + void CameraOff(); + } + + public class CameraSelectedEventArgs : EventArgs + { + public CameraBase SelectedCamera { get; private set; } + + public CameraSelectedEventArgs(CameraBase camera) + { + SelectedCamera = camera; + } + } + + public interface IHasFarEndCameraControl + { + CameraBase FarEndCamera { get; } + + BoolFeedback ControllingFarEndCameraFeedback { get; } + + } + + /// + /// Used to decorate a camera as a far end + /// + public interface IAmFarEndCamera + { + + } + + /// + /// Aggregates the pan, tilt and zoom interfaces + /// + public interface IHasCameraPtzControl : IHasCameraPanControl, IHasCameraTiltControl, IHasCameraZoomControl + { + /// + /// Resets the camera position + /// + void PositionHome(); + } + + /// + /// Interface for camera pan control + /// + public interface IHasCameraPanControl + { + void PanLeft(); + void PanRight(); + void PanStop(); + } + + /// + /// Interface for camera tilt control + /// + public interface IHasCameraTiltControl + { + void TiltDown(); + void TiltUp(); + void TiltStop(); + } + + /// + /// Interface for camera zoom control + /// + public interface IHasCameraZoomControl + { + void ZoomIn(); + void ZoomOut(); + void ZoomStop(); + } + + /// + /// Interface for camera focus control + /// + public interface IHasCameraFocusControl + { + void FocusNear(); + void FocusFar(); + void FocusStop(); + + void TriggerAutoFocus(); + } + + public interface IHasCameraAutoMode + { + void CameraAutoModeOn(); + void CameraAutoModeOff(); + void CameraAutoModeToggle(); + BoolFeedback CameraAutoModeIsOnFeedback { get; } + } + + + + +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasCodecLayouts.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasCodecLayouts.cs new file mode 100644 index 00000000..131d610f --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasCodecLayouts.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + /// + /// Defines the required elements for layout control + /// + public interface IHasCodecLayouts + { + StringFeedback LocalLayoutFeedback { get; } + + void LocalLayoutToggle(); + void LocalLayoutToggleSingleProminent(); + void MinMaxLayoutToggle(); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasCodecSelfview.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasCodecSelfview.cs new file mode 100644 index 00000000..3b079b7d --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasCodecSelfview.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + /// + /// Defines the requred elements for selfview control + /// + public interface IHasCodecSelfView + { + BoolFeedback SelfviewIsOnFeedback { get; } + + bool ShowSelfViewByDefault { get; } + + void SelfViewModeOn(); + + void SelfViewModeOff(); + + void SelfViewModeToggle(); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasVideoCodec.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasVideoCodec.cs new file mode 100644 index 00000000..1d0f4850 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasVideoCodec.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + /// + /// For rooms that have video codec + /// + public interface IHasVideoCodec + { + VideoCodecBase VideoCodec { get; } + BoolFeedback InCallFeedback { get; } + + ///// + ///// Make this more specific + ///// + //List ActiveCalls { get; } + + /// + /// States: 0 for on hook, 1 for video, 2 for audio, 3 for telekenesis + /// + IntFeedback CallTypeFeedback { get; } + + /// + /// + /// + BoolFeedback PrivacyModeIsOnFeedback { get; } + + /// + /// When something in the room is sharing with the far end or through other means + /// + BoolFeedback IsSharingFeedback { get; } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/iVideoCodecInfo.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/iVideoCodecInfo.cs new file mode 100644 index 00000000..902dd68f --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/iVideoCodecInfo.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Devices.Common.Codec +{ + /// + /// Implements a common set of data about a codec + /// + public interface iVideoCodecInfo + { + VideoCodecInfo CodecInfo { get; } + } + + /// + /// Stores general information about a codec + /// + public abstract class VideoCodecInfo + { + public abstract bool MultiSiteOptionIsEnabled { get; } + public abstract string IpAddress { get; } + public abstract string SipPhoneNumber { get; } + public abstract string E164Alias { get; } + public abstract string H323Id { get; } + public abstract string SipUri { get; } + public abstract bool AutoAnswerEnabled { get; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockCodecDirectory.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockCodecDirectory.cs new file mode 100644 index 00000000..41b70661 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockCodecDirectory.cs @@ -0,0 +1,419 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; + + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + public static class MockVideoCodecDirectory + { + public enum eFolderId + { + UnitedStates, + Canada, + NewYork, + Boston, + SanFrancisco, + Denver, + Austin, + Calgary + } + + + /// + /// Aggregates the directory items for all directories into a single directory for searching purposes + /// + public static CodecDirectory CompleteDirectory + { + get + { + var completeDirectory = new CodecDirectory(); + + completeDirectory.AddContactsToDirectory(DirectoryRoot.CurrentDirectoryResults); + completeDirectory.AddContactsToDirectory(UnitedStatesFolderContents.CurrentDirectoryResults); + completeDirectory.AddContactsToDirectory(CanadaFolderContents.CurrentDirectoryResults); + completeDirectory.AddContactsToDirectory(NewYorkFolderContents.CurrentDirectoryResults); + completeDirectory.AddContactsToDirectory(BostonFolderContents.CurrentDirectoryResults); + completeDirectory.AddContactsToDirectory(DenverFolderContents.CurrentDirectoryResults); + completeDirectory.AddContactsToDirectory(AustinFolderContents.CurrentDirectoryResults); + completeDirectory.AddContactsToDirectory(CalgaryFolderContents.CurrentDirectoryResults); + + return completeDirectory; + } + } + + public static CodecDirectory DirectoryRoot + { + get + { + var directory = new CodecDirectory(); + + directory.AddFoldersToDirectory + ( + new List() + { + new DirectoryFolder() + { + FolderId = eFolderId.UnitedStates.ToString(), + Name = "United States", + ParentFolderId = "", + Contacts = null + }, + new DirectoryFolder() + { + FolderId = eFolderId.Canada.ToString(), + Name = "Canada", + ParentFolderId = "", + Contacts = null + } + } + ); + + directory.AddContactsToDirectory + ( + new List() + { + new DirectoryContact() + { + Name = "Corporate Bridge", + ContactMethods = new List() + { + new ContactMethod() + { + ContactMethodId = "c_1", + Number = "site.corp.com", + Device = eContactMethodDevice.Video, + CallType = eContactMethodCallType.Video + } + } + } + } + ); + + return directory; + } + } + + public static CodecDirectory UnitedStatesFolderContents + { + get + { + var directory = new CodecDirectory(); + + directory.ResultsFolderId = eFolderId.UnitedStates.ToString(); + directory.AddFoldersToDirectory + ( + new List() + { + new DirectoryFolder() + { + FolderId = eFolderId.NewYork.ToString(), + Name = "New York", + ParentFolderId = eFolderId.UnitedStates.ToString(), + Contacts = null + }, + new DirectoryFolder() + { + FolderId = eFolderId.Boston.ToString(), + Name = "Boston", + ParentFolderId = eFolderId.UnitedStates.ToString(), + Contacts = null + }, + new DirectoryFolder() + { + FolderId = eFolderId.SanFrancisco.ToString(), + Name = "San Francisco", + ParentFolderId = eFolderId.UnitedStates.ToString(), + Contacts = null + }, + new DirectoryFolder() + { + FolderId = eFolderId.Denver.ToString(), + Name = "Denver", + ParentFolderId = eFolderId.UnitedStates.ToString(), + Contacts = null + }, + new DirectoryFolder() + { + FolderId = eFolderId.Austin.ToString(), + Name = "Austin", + ParentFolderId = eFolderId.UnitedStates.ToString(), + Contacts = null + } + } + ); + + return directory; + } + } + + public static CodecDirectory NewYorkFolderContents + { + get + { + var directory = new CodecDirectory(); + + directory.ResultsFolderId = eFolderId.NewYork.ToString(); + directory.AddContactsToDirectory + ( + new List() + { + new DirectoryContact() + { + ContactId = "nyc_1", + Name = "Meeting Room", + Title = @"", + ContactMethods = new List() + { + new ContactMethod() + { + ContactMethodId = "cid_1", + Number = "nycmeetingroom.pepperdash.com", + Device = eContactMethodDevice.Video, + CallType = eContactMethodCallType.Video + } + } + }, + new DirectoryContact() + { + ContactId = "nyc_2", + Name = "Sumanth Rayancha", + Title = @"CTO", + ContactMethods = new List() + { + new ContactMethod() + { + ContactMethodId = "cid_1", + Number = "srayancha.pepperdash.com", + Device = eContactMethodDevice.Video, + CallType = eContactMethodCallType.Video + } + } + }, + new DirectoryContact() + { + ContactId = "nyc_3", + Name = "Justin Gordon", + Title = @"Software Developer", + ContactMethods = new List() + { + new ContactMethod() + { + ContactMethodId = "cid_1", + Number = "jgordon.pepperdash.com", + Device = eContactMethodDevice.Video, + CallType = eContactMethodCallType.Video + } + } + } + } + ); + + return directory; + } + } + + public static CodecDirectory BostonFolderContents + { + get + { + var directory = new CodecDirectory(); + + directory.ResultsFolderId = eFolderId.Boston.ToString(); + directory.AddContactsToDirectory + ( + new List() + { + new DirectoryContact() + { + ContactId = "bos_1", + Name = "Board Room", + Title = @"", + ContactMethods = new List() + { + new ContactMethod() + { + ContactMethodId = "cid_1", + Number = "bosboardroom.pepperdash.com", + Device = eContactMethodDevice.Video, + CallType = eContactMethodCallType.Video + } + } + } + } + ); + + return directory; + } + } + + public static CodecDirectory SanFranciscoFolderContents + { + get + { + var directory = new CodecDirectory(); + + directory.ResultsFolderId = eFolderId.SanFrancisco.ToString(); + directory.AddContactsToDirectory + ( + new List() + { + new DirectoryContact() + { + ContactId = "sfo_1", + Name = "David Huselid", + Title = @"Cive President, COO", + ContactMethods = new List() + { + new ContactMethod() + { + ContactMethodId = "cid_1", + Number = "dhuselid.pepperdash.com", + Device = eContactMethodDevice.Video, + CallType = eContactMethodCallType.Video + } + } + } + } + ); + + return directory; + } + } + + public static CodecDirectory DenverFolderContents + { + get + { + var directory = new CodecDirectory(); + + directory.ResultsFolderId = eFolderId.Denver.ToString(); + directory.AddContactsToDirectory + ( + new List() + { + new DirectoryContact() + { + ContactId = "den_1", + Name = "Heath Volmer", + Title = @"Software Developer", + ContactMethods = new List() + { + new ContactMethod() + { + ContactMethodId = "cid_1", + Number = "hvolmer.pepperdash.com", + Device = eContactMethodDevice.Video, + CallType = eContactMethodCallType.Video + } + } + } + } + ); + + return directory; + } + } + + public static CodecDirectory AustinFolderContents + { + get + { + var directory = new CodecDirectory(); + + directory.ResultsFolderId = eFolderId.Austin.ToString(); + directory.AddContactsToDirectory + ( + new List() + { + new DirectoryContact() + { + ContactId = "atx_1", + Name = "Vincent Longano", + Title = @"Product Development Manager", + ContactMethods = new List() + { + new ContactMethod() + { + ContactMethodId = "cid_1", + Number = "vlongano.pepperdash.com", + Device = eContactMethodDevice.Video, + CallType = eContactMethodCallType.Video + } + } + } + } + ); + + return directory; + } + } + + public static CodecDirectory CanadaFolderContents + { + get + { + var directory = new CodecDirectory(); + + directory.ResultsFolderId = eFolderId.Canada.ToString(); + directory.AddFoldersToDirectory + ( + new List() + { + new DirectoryFolder() + { + FolderId = eFolderId.Calgary.ToString(), + Name = "Calgary", + ParentFolderId = eFolderId.Canada.ToString(), + Contacts = null + } + } + ); + + return directory; + } + } + + public static CodecDirectory CalgaryFolderContents + { + get + { + var directory = new CodecDirectory(); + + directory.ResultsFolderId = eFolderId.Calgary.ToString(); + directory.AddContactsToDirectory + ( + new List() + { + new DirectoryContact() + { + ContactId = "cdn_1", + Name = "Neil Dorin", + Title = @"Software Developer /SC", + ContactMethods = new List() + { + new ContactMethod() + { + ContactMethodId = "cid_1", + Number = "ndorin@pepperdash.com", + Device = eContactMethodDevice.Video, + CallType = eContactMethodCallType.Video + } + } + } + } + ); + + return directory; + } + } + + + } +} \ 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 new file mode 100644 index 00000000..386da4f6 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVC.cs @@ -0,0 +1,773 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.Cameras; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + public class MockVC : VideoCodecBase, IRoutingSource, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets + { + public MockVcPropertiesConfig PropertiesConfig; + + public RoutingInputPort CodecOsdIn { get; private set; } + public RoutingInputPort HdmiIn1 { get; private set; } + public RoutingInputPort HdmiIn2 { get; private set; } + public RoutingOutputPort HdmiOut { get; private set; } + + public CodecCallFavorites CallFavorites { get; private set; } + + /// + /// + /// + public MockVC(DeviceConfig config) + : base(config) + { + PropertiesConfig = JsonConvert.DeserializeObject(config.Properties.ToString()); + + CodecInfo = new MockCodecInfo(); + + // Get favoritesw + if (PropertiesConfig.Favorites != null) + { + CallFavorites = new CodecCallFavorites(); + CallFavorites.Favorites = PropertiesConfig.Favorites; + } + + DirectoryBrowseHistory = new List(); + + // Debug helpers + MuteFeedback.OutputChange += (o, a) => Debug.Console(1, this, "Mute={0}", _IsMuted); + PrivacyModeIsOnFeedback.OutputChange += (o, a) => Debug.Console(1, this, "Privacy={0}", _PrivacyModeIsOn); + SharingSourceFeedback.OutputChange += (o, a) => Debug.Console(1, this, "SharingSource={0}", _SharingSource); + VolumeLevelFeedback.OutputChange += (o, a) => Debug.Console(1, this, "Volume={0}", _VolumeLevel); + + CurrentDirectoryResultIsNotDirectoryRoot = new BoolFeedback(() => DirectoryBrowseHistory.Count > 0); + + CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate(); + + CodecOsdIn = new RoutingInputPort(RoutingPortNames.CodecOsd, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 0, this); + InputPorts.Add(CodecOsdIn); + HdmiIn1 = new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 1, this); + InputPorts.Add(HdmiIn1); + HdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, 2, this); + InputPorts.Add(HdmiIn2); + HdmiOut = new RoutingOutputPort(RoutingPortNames.HdmiOut, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this); + OutputPorts.Add(HdmiOut); + + CallHistory = new CodecCallHistory(); + for (int i = 0; i < 10; i++) + { + var call = new CodecCallHistory.CallHistoryEntry(); + call.Name = "Call " + i; + call.Number = i + "@call.com"; + CallHistory.RecentCalls.Add(call); + } + // eventually fire history event here + + SetupCameras(); + + SetIsReady(); + } + + protected override Func MuteFeedbackFunc + { + get { return () => _IsMuted; } + } + bool _IsMuted; + + protected override Func PrivacyModeIsOnFeedbackFunc + { + get { return () => _PrivacyModeIsOn; } + } + bool _PrivacyModeIsOn; + + protected override Func SharingSourceFeedbackFunc + { + get { return () => _SharingSource; } + } + string _SharingSource; + + protected override Func SharingContentIsOnFeedbackFunc + { + get { return () => _SharingIsOn; } + } + bool _SharingIsOn; + + protected override Func VolumeLevelFeedbackFunc + { + get { return () => _VolumeLevel; } + } + int _VolumeLevel; + + protected override Func StandbyIsOnFeedbackFunc + { + get { return () => _StandbyIsOn; } + } + bool _StandbyIsOn; + + + /// + /// Dials, yo! + /// + public override void Dial(string number) + { + Debug.Console(1, this, "Dial: {0}", number); + var call = new CodecActiveCallItem() { Name = number, Number = number, Id = number, Status = eCodecCallStatus.Dialing, Direction = eCodecCallDirection.Outgoing, Type = eCodecCallType.Video }; + ActiveCalls.Add(call); + OnCallStatusChange(call); + //ActiveCallCountFeedback.FireUpdate(); + // Simulate 2-second ring, then connecting, then connected + new CTimer(o => + { + call.Type = eCodecCallType.Video; + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call); + new CTimer(oo => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call), 1000); + }, 2000); + } + + public override void Dial(Meeting meeting) + { + throw new NotImplementedException(); + } + + /// + /// + /// + public override void EndCall(CodecActiveCallItem call) + { + Debug.Console(1, this, "EndCall"); + ActiveCalls.Remove(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); + //ActiveCallCountFeedback.FireUpdate(); + } + + /// + /// + /// + public override void EndAllCalls() + { + Debug.Console(1, this, "EndAllCalls"); + for(int i = ActiveCalls.Count - 1; i >= 0; i--) + { + var call = ActiveCalls[i]; + ActiveCalls.Remove(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); + } + //ActiveCallCountFeedback.FireUpdate(); + } + + /// + /// For a call from the test methods below + /// + public override void AcceptCall(CodecActiveCallItem call) + { + Debug.Console(1, this, "AcceptCall"); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call); + new CTimer(o => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call), 1000); + // should already be in active list + } + + /// + /// For a call from the test methods below + /// + public override void RejectCall(CodecActiveCallItem call) + { + Debug.Console(1, this, "RejectCall"); + ActiveCalls.Remove(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); + //ActiveCallCountFeedback.FireUpdate(); + } + + /// + /// Makes horrible tones go out on the wire! + /// + /// + public override void SendDtmf(string s) + { + Debug.Console(1, this, "SendDTMF: {0}", s); + } + + /// + /// + /// + public override void StartSharing() + { + _SharingIsOn = true; + SharingContentIsOnFeedback.FireUpdate(); + } + + /// + /// + /// + public override void StopSharing() + { + _SharingIsOn = false; + SharingContentIsOnFeedback.FireUpdate(); + } + + public override void StandbyActivate() + { + _StandbyIsOn = true; + } + + public override void StandbyDeactivate() + { + _StandbyIsOn = false; + } + + /// + /// Called by routing to make it happen + /// + /// + public override void ExecuteSwitch(object selector) + { + Debug.Console(1, this, "ExecuteSwitch: {0}", selector); + _SharingSource = selector.ToString(); + } + + /// + /// + /// + public override void MuteOff() + { + _IsMuted = false; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public override void MuteOn() + { + _IsMuted = true; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public override void MuteToggle() + { + _IsMuted = !_IsMuted; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + /// + public override void SetVolume(ushort level) + { + _VolumeLevel = level; + VolumeLevelFeedback.FireUpdate(); + } + + /// + /// + /// + /// + public override void VolumeDown(bool pressRelease) + { + } + + /// + /// + /// + /// + public override void VolumeUp(bool pressRelease) + { + } + + /// + /// + /// + public override void PrivacyModeOn() + { + Debug.Console(1, this, "PrivacyMuteOn"); + if (_PrivacyModeIsOn) + return; + _PrivacyModeIsOn = true; + PrivacyModeIsOnFeedback.FireUpdate(); + } + + /// + /// + /// + public override void PrivacyModeOff() + { + Debug.Console(1, this, "PrivacyMuteOff"); + if (!_PrivacyModeIsOn) + return; + _PrivacyModeIsOn = false; + PrivacyModeIsOnFeedback.FireUpdate(); + } + + /// + /// + /// + public override void PrivacyModeToggle() + { + _PrivacyModeIsOn = !_PrivacyModeIsOn; + Debug.Console(1, this, "PrivacyMuteToggle: {0}", _PrivacyModeIsOn); + PrivacyModeIsOnFeedback.FireUpdate(); + } + + //******************************************************** + // SIMULATION METHODS + + /// + /// + /// + /// + public void TestIncomingVideoCall(string url) + { + Debug.Console(1, this, "TestIncomingVideoCall from {0}", url); + var call = new CodecActiveCallItem() { Name = url, Id = url, Number = url, Type= eCodecCallType.Video, Direction = eCodecCallDirection.Incoming }; + ActiveCalls.Add(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Ringing, call); + + //OnCallStatusChange(eCodecCallStatus.Unknown, eCodecCallStatus.Ringing, call); + + } + + /// + /// + /// + /// + public void TestIncomingAudioCall(string url) + { + Debug.Console(1, this, "TestIncomingAudioCall from {0}", url); + var call = new CodecActiveCallItem() { Name = url, Id = url, Number = url, Type = eCodecCallType.Audio, Direction = eCodecCallDirection.Incoming }; + ActiveCalls.Add(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Ringing, call); + + //OnCallStatusChange(eCodecCallStatus.Unknown, eCodecCallStatus.Ringing, call); + } + + /// + /// + /// + public void TestFarEndHangup() + { + Debug.Console(1, this, "TestFarEndHangup"); + + } + + + #region IHasCallHistory Members + + public CodecCallHistory CallHistory { get; private set; } + + public void RemoveCallHistoryEntry(CodecCallHistory.CallHistoryEntry entry) + { + + } + + #endregion + + #region IHasScheduleAwareness Members + + public void GetSchedule() + { + + } + + public CodecScheduleAwareness CodecSchedule + { + get { + // if the last meeting has past, generate a new list + if (_CodecSchedule == null || _CodecSchedule.Meetings.Count == 0 + || _CodecSchedule.Meetings[_CodecSchedule.Meetings.Count - 1].StartTime < DateTime.Now) + { + _CodecSchedule = new CodecScheduleAwareness(); + for (int i = 0; i < 5; i++) + { + var m = new Meeting(); + m.StartTime = DateTime.Now.AddMinutes(3).AddHours(i); + m.EndTime = DateTime.Now.AddHours(i).AddMinutes(30); + m.Title = "Meeting " + i; + m.Calls.Add(new Call() { Number = i + "meeting@fake.com"}); + _CodecSchedule.Meetings.Add(m); + } + } + return _CodecSchedule; + } + } + CodecScheduleAwareness _CodecSchedule; + + #endregion + + #region IHasDirectory Members + + public event EventHandler DirectoryResultReturned; + + + public CodecDirectory DirectoryRoot + { + get + { + return MockVideoCodecDirectory.DirectoryRoot; + } + } + + public CodecDirectory CurrentDirectoryResult + { + get + { + if (DirectoryBrowseHistory.Count > 0) + return DirectoryBrowseHistory[DirectoryBrowseHistory.Count - 1]; + else + return DirectoryRoot; + } + } + + public CodecPhonebookSyncState PhonebookSyncState + { + get + { + var syncState = new CodecPhonebookSyncState(Key + "PhonebookSync"); + + syncState.InitialPhonebookFoldersReceived(); + syncState.PhonebookRootEntriesReceived(); + syncState.SetPhonebookHasFolders(true); + syncState.SetNumberOfContacts(0); // just need to call this method for the sync to complete + + return syncState; + } + } + + public void SearchDirectory(string searchString) + { + var searchResults = new CodecDirectory(); + + searchResults.ResultsFolderId = "searchResult"; + + // Search mock directory for contacts that contain the search string, ignoring case + List matches = MockVideoCodecDirectory.CompleteDirectory.CurrentDirectoryResults.FindAll( + s => s is DirectoryContact && s.Name.ToLower().Contains(searchString.ToLower())); + + if (matches != null) + { + searchResults.AddContactsToDirectory(matches); + + DirectoryBrowseHistory.Add(searchResults); + } + + OnDirectoryResultReturned(searchResults); + } + + public void GetDirectoryFolderContents(string folderId) + { + var folderDirectory = new CodecDirectory(); + + if (folderId == MockVideoCodecDirectory.eFolderId.UnitedStates.ToString()) + folderDirectory = MockVideoCodecDirectory.UnitedStatesFolderContents; + else if (folderId == MockVideoCodecDirectory.eFolderId.Canada.ToString()) + folderDirectory = MockVideoCodecDirectory.CanadaFolderContents; + else if (folderId == MockVideoCodecDirectory.eFolderId.NewYork.ToString()) + folderDirectory = MockVideoCodecDirectory.NewYorkFolderContents; + else if (folderId == MockVideoCodecDirectory.eFolderId.Boston.ToString()) + folderDirectory = MockVideoCodecDirectory.BostonFolderContents; + else if (folderId == MockVideoCodecDirectory.eFolderId.SanFrancisco.ToString()) + folderDirectory = MockVideoCodecDirectory.SanFranciscoFolderContents; + else if (folderId == MockVideoCodecDirectory.eFolderId.Denver.ToString()) + folderDirectory = MockVideoCodecDirectory.DenverFolderContents; + else if (folderId == MockVideoCodecDirectory.eFolderId.Austin.ToString()) + folderDirectory = MockVideoCodecDirectory.AustinFolderContents; + else if (folderId == MockVideoCodecDirectory.eFolderId.Calgary.ToString()) + folderDirectory = MockVideoCodecDirectory.CalgaryFolderContents; + + DirectoryBrowseHistory.Add(folderDirectory); + + OnDirectoryResultReturned(folderDirectory); + } + + public void SetCurrentDirectoryToRoot() + { + DirectoryBrowseHistory.Clear(); + + OnDirectoryResultReturned(DirectoryRoot); + } + + 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); + } + + public BoolFeedback CurrentDirectoryResultIsNotDirectoryRoot { get; private set; } + + public List DirectoryBrowseHistory { get; private set; } + + public void OnDirectoryResultReturned(CodecDirectory result) + { + CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate(); + + var handler = DirectoryResultReturned; + if (handler != null) + { + handler(this, new DirectoryEventArgs() + { + Directory = result, + DirectoryIsOnRoot = !CurrentDirectoryResultIsNotDirectoryRoot.BoolValue + }); + } + } + + #endregion + + void SetupCameras() + { + Cameras = new List(); + + var internalCamera = new MockVCCamera(Key + "-camera1", "Near End", this); + + Cameras.Add(internalCamera); + + var farEndCamera = new MockFarEndVCCamera(Key + "-cameraFar", "Far End", this); + + Cameras.Add(farEndCamera); + + SelectedCameraFeedback = new StringFeedback(() => SelectedCamera.Key); + + ControllingFarEndCameraFeedback = new BoolFeedback(() => SelectedCamera is IAmFarEndCamera); + + CameraAutoModeIsOnFeedback = new BoolFeedback(() => _CameraAutoModeIsOn); + + CameraAutoModeIsOnFeedback.FireUpdate(); + + DeviceManager.AddDevice(internalCamera); + DeviceManager.AddDevice(farEndCamera); + + NearEndPresets = new List(15); // Fix the capacity to emulate Cisco + + NearEndPresets = PropertiesConfig.Presets; + + FarEndRoomPresets = new List(15); // Fix the capacity to emulate Cisco + + // 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 IHasCameras Members + + public event EventHandler CameraSelected; + + public List Cameras { get; private set; } + + private CameraBase _selectedCamera; + + /// + /// Returns the selected camera + /// + public CameraBase SelectedCamera + { + get + { + return _selectedCamera; + } + private set + { + _selectedCamera = value; + SelectedCameraFeedback.FireUpdate(); + ControllingFarEndCameraFeedback.FireUpdate(); + + var handler = CameraSelected; + if (handler != null) + { + handler(this, new CameraSelectedEventArgs(SelectedCamera)); + } + } + } + + public StringFeedback SelectedCameraFeedback { get; private set; } + + public void SelectCamera(string key) + { + var camera = Cameras.FirstOrDefault(c => c.Key.ToLower().IndexOf(key.ToLower()) > -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); + } + + #endregion + + #region IHasFarEndCameraControl Members + + public CameraBase FarEndCamera { get; private set; } + + public BoolFeedback ControllingFarEndCameraFeedback { get; private set; } + + #endregion + + #region IHasCameraAutoMode Members + + private bool _CameraAutoModeIsOn; + + public void CameraAutoModeOn() + { + _CameraAutoModeIsOn = true; + CameraAutoModeIsOnFeedback.FireUpdate(); + } + + public void CameraAutoModeOff() + { + _CameraAutoModeIsOn = false; + CameraAutoModeIsOnFeedback.FireUpdate(); + } + + public void CameraAutoModeToggle() + { + if(_CameraAutoModeIsOn) + _CameraAutoModeIsOn = false; + else + _CameraAutoModeIsOn = true; + + CameraAutoModeIsOnFeedback.FireUpdate(); + + } + + public BoolFeedback CameraAutoModeIsOnFeedback {get; private set;} + + #endregion + + #region IHasCameraPresets Members + + public event EventHandler CodecRoomPresetsListHasChanged; + + public List NearEndPresets { get; private set; } + + public List FarEndRoomPresets { get; private set; } + + public void CodecRoomPresetSelect(int preset) + { + if (SelectedCamera is IAmFarEndCamera) + { + Debug.Console(1, this, "Selecting Far End Preset: {0}", preset); + } + else + { + Debug.Console(1, this, "Selecting Near End Preset: {0}", preset); + } + } + + public void CodecRoomPresetStore(int preset, string description) + { + var editPreset = NearEndPresets.FirstOrDefault(p => p.ID.Equals(preset)); + + if (editPreset != null) + { + editPreset.Defined = true; + editPreset.Description = description; + } + else + NearEndPresets.Add(new CodecRoomPreset(preset, description, true, true)); + + var handler = CodecRoomPresetsListHasChanged; + if (handler != null) + { + handler(this, new EventArgs()); + } + + // Update the config + SetConfig(Config); + } + + #endregion + + protected override void CustomSetConfig(DeviceConfig config) + { + PropertiesConfig.Presets = NearEndPresets; + + Config.Properties = JToken.FromObject(PropertiesConfig); + + ConfigWriter.UpdateDeviceConfig(config); + } + + } + + /// + /// Implementation for the mock VC + /// + public class MockCodecInfo : VideoCodecInfo + { + + public override bool MultiSiteOptionIsEnabled + { + get { return true; } + } + + public override string E164Alias + { + get { return "someE164alias"; } + } + + public override string H323Id + { + get { return "someH323Id"; } + } + + public override string IpAddress + { + get { return "xxx.xxx.xxx.xxx"; } + } + + public override string SipPhoneNumber + { + get { return "333-444-5555"; } + } + + public override string SipUri + { + get { return "mock@someurl.com"; } + } + + public override bool AutoAnswerEnabled + { + get { return _AutoAnswerEnabled; } + } + bool _AutoAnswerEnabled; + + public void SetAutoAnswer(bool value) + { + _AutoAnswerEnabled = value; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVC.cs.orig b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVC.cs.orig new file mode 100644 index 00000000..f70988dd --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVC.cs.orig @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + public class MockVC : VideoCodecBase + { + public MockVC(string key, string name) + : base(key, name) + { + // Debug helpers + ActiveCallCountFeedback.OutputChange += (o, a) => Debug.Console(1, this, "InCall={0}", ActiveCallCountFeedback.IntValue); + IncomingCallFeedback.OutputChange += (o, a) => Debug.Console(1, this, "IncomingCall={0}", _IncomingCall); + MuteFeedback.OutputChange += (o, a) => Debug.Console(1, this, "Mute={0}", _IsMuted); + PrivacyModeIsOnFeedback.OutputChange += (o, a) => Debug.Console(1, this, "Privacy={0}", _PrivacyModeIsOn); + SharingSourceFeedback.OutputChange += (o, a) => Debug.Console(1, this, "SharingSource={0}", _SharingSource); + VolumeLevelFeedback.OutputChange += (o, a) => Debug.Console(1, this, "Volume={0}", _VolumeLevel); + } + + protected override Func ActiveCallCountFeedbackFunc + { + get { return () => ActiveCalls.Count; } + } + + protected override Func IncomingCallFeedbackFunc + { + get { return () => _IncomingCall; } + } + bool _IncomingCall; + + protected override Func MuteFeedbackFunc + { + get { return () => _IsMuted; } + } + bool _IsMuted; + + protected override Func PrivacyModeIsOnFeedbackFunc + { + get { return () => _PrivacyModeIsOn; } + } + bool _PrivacyModeIsOn; + + protected override Func SharingSourceFeedbackFunc + { + get { return () => _SharingSource; } + } + string _SharingSource; + + protected override Func VolumeLevelFeedbackFunc + { + get { return () => _VolumeLevel; } + } + int _VolumeLevel; + + /// + /// Dials, yo! + /// + public override void Dial(string s) + { + Debug.Console(1, this, "Dial: {0}", s); + ActiveCalls.Add(new CodecActiveCallItem(s,s, eCodecCallType.Video)); + ActiveCallCountFeedback.FireUpdate(); + } + + /// + /// + /// + public override void EndCall(CodecActiveCallItem activeCall) + { + Debug.Console(1, this, "EndCall"); + ActiveCalls.RemoveAll(i => i.Name == s); + ActiveCallCountFeedback.FireUpdate(); + //_InCall = false; + //IsInCall.FireUpdate(); + } + + public override void EndAllCalls() + { + + } + + /// + /// For a call from the test methods below + /// + public override void AcceptCall() + { + Debug.Console(1, this, "AcceptCall"); + } + + /// + /// For a call from the test methods below + /// + public override void RejectCall() + { + Debug.Console(1, this, "RejectCall"); + } + + /// + /// Makes horrible tones go out on the wire! + /// + /// + public override void SendDtmf(string s) + { + Debug.Console(1, this, "SendDTMF: {0}", s); + } + + /// + /// + /// + public override void StartSharing() + { + } + + /// + /// + /// + public override void StopSharing() + { + } + + /// + /// Called by routing to make it happen + /// + /// + public override void ExecuteSwitch(object selector) + { + Debug.Console(1, this, "ExecuteSwitch"); + _SharingSource = selector.ToString(); + + } + + /// + /// + /// + public override void MuteOff() + { + _IsMuted = false; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public override void MuteOn() + { + _IsMuted = true; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + public override void MuteToggle() + { + _IsMuted = !_IsMuted; + MuteFeedback.FireUpdate(); + } + + /// + /// + /// + /// + public override void SetVolume(ushort level) + { + _VolumeLevel = level; + VolumeLevelFeedback.FireUpdate(); + } + + /// + /// + /// + /// + public override void VolumeDown(bool pressRelease) + { + } + + /// + /// + /// + /// + public override void VolumeUp(bool pressRelease) + { + } + + /// + /// + /// + public override void PrivacyModeOn() + { + Debug.Console(1, this, "PrivacyMuteOn"); + if (_PrivacyModeIsOn) + return; + _PrivacyModeIsOn = true; + PrivacyModeIsOnFeedback.FireUpdate(); + + } + + /// + /// + /// + public override void PrivacyModeOff() + { + Debug.Console(1, this, "PrivacyMuteOff"); + if (!_PrivacyModeIsOn) + return; + _PrivacyModeIsOn = false; + PrivacyModeIsOnFeedback.FireUpdate(); + } + + /// + /// + /// + public override void PrivacyModeToggle() + { + _PrivacyModeIsOn = !_PrivacyModeIsOn; + Debug.Console(1, this, "PrivacyMuteToggle: {0}", _PrivacyModeIsOn); + PrivacyModeIsOnFeedback.FireUpdate(); + } + + //******************************************************** + // SIMULATION METHODS + + /// + /// + /// + /// + public void TestIncomingCall(string url) + { + Debug.Console(1, this, "TestIncomingCall"); + + _IncomingCall = true; + IncomingCallFeedback.FireUpdate(); + } + + /// + /// + /// + public void TestFarEndHangup() + { + Debug.Console(1, this, "TestFarEndHangup"); + + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVCCamera.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVCCamera.cs new file mode 100644 index 00000000..c404ac89 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVCCamera.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.Devices.Common.Cameras +{ + public class MockVCCamera : CameraBase, IHasCameraPtzControl, IHasCameraFocusControl + { + protected VideoCodecBase ParentCodec { get; private set; } + + + public MockVCCamera(string key, string name, VideoCodecBase codec) + : base(key, name) + { + Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom | eCameraCapabilities.Focus; + + ParentCodec = codec; + } + + #region IHasCameraPtzControl Members + + public void PositionHome() + { + Debug.Console(1, this, "Resetting to home position"); + } + + #endregion + + #region IHasCameraPanControl Members + + public void PanLeft() + { + Debug.Console(1, this, "Panning Left"); + } + + public void PanRight() + { + Debug.Console(1, this, "Panning Right"); + } + + public void PanStop() + { + Debug.Console(1, this, "Stopping Pan"); + } + + #endregion + + #region IHasCameraTiltControl Members + + public void TiltDown() + { + Debug.Console(1, this, "Tilting Down"); + } + + public void TiltUp() + { + Debug.Console(1, this, "Tilting Up"); + } + + public void TiltStop() + { + Debug.Console(1, this, "Stopping Tilt"); + } + + #endregion + + #region IHasCameraZoomControl Members + + public void ZoomIn() + { + Debug.Console(1, this, "Zooming In"); + } + + public void ZoomOut() + { + Debug.Console(1, this, "Zooming Out"); + } + + public void ZoomStop() + { + Debug.Console(1, this, "Stopping Zoom"); + } + + #endregion + + #region IHasCameraFocusControl Members + + public void FocusNear() + { + Debug.Console(1, this, "Focusing Near"); + } + + public void FocusFar() + { + Debug.Console(1, this, "Focusing Far"); + } + + public void FocusStop() + { + Debug.Console(1, this, "Stopping Focus"); + } + + public void TriggerAutoFocus() + { + Debug.Console(1, this, "AutoFocus Triggered"); + } + + #endregion + } + + public class MockFarEndVCCamera : CameraBase, IHasCameraPtzControl, IAmFarEndCamera + { + protected VideoCodecBase ParentCodec { get; private set; } + + + public MockFarEndVCCamera(string key, string name, VideoCodecBase codec) + : base(key, name) + { + Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom; + + ParentCodec = codec; + } + + #region IHasCameraPtzControl Members + + public void PositionHome() + { + Debug.Console(1, this, "Resetting to home position"); + } + + #endregion + + #region IHasCameraPanControl Members + + public void PanLeft() + { + Debug.Console(1, this, "Panning Left"); + } + + public void PanRight() + { + Debug.Console(1, this, "Panning Right"); + } + + public void PanStop() + { + Debug.Console(1, this, "Stopping Pan"); + } + + #endregion + + #region IHasCameraTiltControl Members + + public void TiltDown() + { + Debug.Console(1, this, "Tilting Down"); + } + + public void TiltUp() + { + Debug.Console(1, this, "Tilting Up"); + } + + public void TiltStop() + { + Debug.Console(1, this, "Stopping Tilt"); + } + + #endregion + + #region IHasCameraZoomControl Members + + public void ZoomIn() + { + Debug.Console(1, this, "Zooming In"); + } + + public void ZoomOut() + { + Debug.Console(1, this, "Zooming Out"); + } + + public void ZoomStop() + { + Debug.Console(1, this, "Stopping Zoom"); + } + + #endregion + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..6a790af6 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVcPropertiesConfig.cs @@ -0,0 +1,22 @@ +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.Devices.Common.Codec; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + public class MockVcPropertiesConfig + { + [JsonProperty("favorites")] + public List Favorites { get; set; } + + [JsonProperty("presets")] + public List Presets { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs new file mode 100644 index 00000000..1f8938f6 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.Devices; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Devices.Common; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs, + IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo + { + /// + /// Fires when the status of any active, dialing, or incoming call changes or is new + /// + public event EventHandler CallStatusChange; + + public event EventHandler IsReadyChange; + + public IBasicCommunication Communication { get; protected set; } + + #region IUsageTracking Members + + /// + /// This object can be added by outside users of this class to provide usage tracking + /// for various services + /// + public UsageTracking UsageTracker { get; set; } + + #endregion + + /// + /// An internal pseudo-source that is routable and connected to the osd input + /// + public DummyRoutingInputsDevice OsdSource { get; protected set; } + + public RoutingPortCollection InputPorts { get; private set; } + + public RoutingPortCollection OutputPorts { get; private set; } + + /// + /// Returns true when any call is not in state Unknown, Disconnecting, Disconnected + /// + public bool IsInCall + { + get + { + bool value; + + if (ActiveCalls != null) + value = ActiveCalls.Any(c => c.IsActiveCall); + else + value = false; + return value; + } + } + + public BoolFeedback StandbyIsOnFeedback { get; private set; } + + abstract protected Func PrivacyModeIsOnFeedbackFunc { get; } + abstract protected Func VolumeLevelFeedbackFunc { get; } + abstract protected Func MuteFeedbackFunc { get; } + abstract protected Func StandbyIsOnFeedbackFunc { get; } + + public List ActiveCalls { get; set; } + + public VideoCodecInfo CodecInfo { get; protected set; } + + public bool ShowSelfViewByDefault { get; protected set; } + + + public bool IsReady { get; protected set; } + + public VideoCodecBase(DeviceConfig config) + : base(config) + { + StandbyIsOnFeedback = new BoolFeedback(StandbyIsOnFeedbackFunc); + PrivacyModeIsOnFeedback = new BoolFeedback(PrivacyModeIsOnFeedbackFunc); + VolumeLevelFeedback = new IntFeedback(VolumeLevelFeedbackFunc); + MuteFeedback = new BoolFeedback(MuteFeedbackFunc); + SharingSourceFeedback = new StringFeedback(SharingSourceFeedbackFunc); + SharingContentIsOnFeedback = new BoolFeedback(SharingContentIsOnFeedbackFunc); + + InputPorts = new RoutingPortCollection(); + OutputPorts = new RoutingPortCollection(); + + ActiveCalls = new List(); + } + + #region IHasDialer Members + + public abstract void Dial(string number); + public abstract void Dial(Meeting meeting); + public virtual void Dial(IInvitableContact contact) + { + + } + public abstract void EndCall(CodecActiveCallItem call); + public abstract void EndAllCalls(); + public abstract void AcceptCall(CodecActiveCallItem call); + public abstract void RejectCall(CodecActiveCallItem call); + public abstract void SendDtmf(string s); + + #endregion + + public virtual List Feedbacks + { + get + { + return new List + { + PrivacyModeIsOnFeedback, + SharingSourceFeedback + }; + } + } + + public abstract void ExecuteSwitch(object selector); + + /// + /// Helper method to fire CallStatusChange event with old and new status + /// + protected void SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus newStatus, CodecActiveCallItem call) + { + call.Status = newStatus; + + OnCallStatusChange(call); + + } + + /// + /// + /// + /// + /// + /// + protected void OnCallStatusChange(CodecActiveCallItem item) + { + var handler = CallStatusChange; + if (handler != null) + handler(this, new CodecCallStatusItemChangeEventArgs(item)); + + if (AutoShareContentWhileInCall) + StartSharing(); + + if (UsageTracker != null) + { + if (IsInCall && !UsageTracker.UsageTrackingStarted) + UsageTracker.StartDeviceUsage(); + else if (UsageTracker.UsageTrackingStarted && !IsInCall) + UsageTracker.EndDeviceUsage(); + } + } + + /// + /// Sets IsReady property and fires the event. Used for dependent classes to sync up their data. + /// + protected void SetIsReady() + { + IsReady = true; + var h = IsReadyChange; + if(h != null) + h(this, new EventArgs()); + } + + #region ICodecAudio Members + + public abstract void PrivacyModeOn(); + public abstract void PrivacyModeOff(); + public abstract void PrivacyModeToggle(); + public BoolFeedback PrivacyModeIsOnFeedback { get; private set; } + + + public BoolFeedback MuteFeedback { get; private set; } + + public abstract void MuteOff(); + + public abstract void MuteOn(); + + public abstract void SetVolume(ushort level); + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public abstract void MuteToggle(); + + public abstract void VolumeDown(bool pressRelease); + + + public abstract void VolumeUp(bool pressRelease); + + #endregion + + #region IHasSharing Members + + public abstract void StartSharing(); + public abstract void StopSharing(); + + public bool AutoShareContentWhileInCall { get; protected set; } + + public StringFeedback SharingSourceFeedback { get; private set; } + public BoolFeedback SharingContentIsOnFeedback { get; private set; } + + abstract protected Func SharingSourceFeedbackFunc { get; } + abstract protected Func SharingContentIsOnFeedbackFunc { get; } + + + #endregion + + // **** DEBUGGING THINGS **** + /// + /// + /// + public virtual void ListCalls() + { + var sb = new StringBuilder(); + foreach (var c in ActiveCalls) + sb.AppendFormat("{0} {1} -- {2} {3}\n", c.Id, c.Number, c.Name, c.Status); + Debug.Console(1, this, "\n{0}\n", sb.ToString()); + } + + public abstract void StandbyActivate(); + + public abstract void StandbyDeactivate(); + + } + + + /// + /// Used to track the status of syncronizing the phonebook values when connecting to a codec or refreshing the phonebook info + /// + public class CodecPhonebookSyncState : 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 InitialPhonebookFoldersWasReceived { get; private set; } + + public bool NumberOfContactsWasReceived { get; private set; } + + public bool PhonebookRootEntriesWasRecieved { get; private set; } + + public bool PhonebookHasFolders { get; private set; } + + public int NumberOfContacts { get; private set; } + + public CodecPhonebookSyncState(string key) + { + Key = key; + + CodecDisconnected(); + } + + public void InitialPhonebookFoldersReceived() + { + InitialPhonebookFoldersWasReceived = true; + + CheckSyncStatus(); + } + + public void PhonebookRootEntriesReceived() + { + PhonebookRootEntriesWasRecieved = true; + + CheckSyncStatus(); + } + + public void SetPhonebookHasFolders(bool value) + { + PhonebookHasFolders = value; + + Debug.Console(1, this, "Phonebook has folders: {0}", PhonebookHasFolders); + } + + public void SetNumberOfContacts(int contacts) + { + NumberOfContacts = contacts; + NumberOfContactsWasReceived = true; + + Debug.Console(1, this, "Phonebook contains {0} contacts.", NumberOfContacts); + + CheckSyncStatus(); + } + + public void CodecDisconnected() + { + InitialPhonebookFoldersWasReceived = false; + PhonebookHasFolders = false; + NumberOfContacts = 0; + NumberOfContactsWasReceived = false; + } + + void CheckSyncStatus() + { + if (InitialPhonebookFoldersWasReceived && NumberOfContactsWasReceived && PhonebookRootEntriesWasRecieved) + { + InitialSyncComplete = true; + Debug.Console(1, this, "Initial Phonebook Sync Complete!"); + } + else + InitialSyncComplete = false; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs.orig b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs.orig new file mode 100644 index 00000000..e13bb133 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs.orig @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec +{ + public abstract class VideoCodecBase : Device, IRoutingSinkWithSwitching, IUsageTracking, IHasDialer, IHasSharing, ICodecAudio + { + #region IUsageTracking Members + + /// + /// This object can be added by outside users of this class to provide usage tracking + /// for various services + /// + public UsageTracking UsageTracker { get; set; } + + #endregion + + #region IRoutingInputs Members + + public RoutingPortCollection InputPorts { get; private set; } + + #endregion + + /// + /// Returns true when ActiveCallCountFeedback is > 0 + /// + public bool IsInCall { get { return ActiveCallCountFeedback.IntValue > 0; } } + + public BoolFeedback IncomingCallFeedback { get; private set; } + + public IntFeedback ActiveCallCountFeedback { get; private set; } + + abstract protected Func ActiveCallCountFeedbackFunc { get; } + abstract protected Func IncomingCallFeedbackFunc { get; } + abstract protected Func PrivacyModeIsOnFeedbackFunc { get; } + abstract protected Func VolumeLevelFeedbackFunc { get; } + abstract protected Func MuteFeedbackFunc { get; } + abstract protected Func SharingSourceFeedbackFunc { get; } + + public List ActiveCalls { get; set; } + + public VideoCodecBase(string key, string name) + : base(key, name) + { + ActiveCallCountFeedback = new IntFeedback(ActiveCallCountFeedbackFunc); + IncomingCallFeedback = new BoolFeedback(IncomingCallFeedbackFunc); + PrivacyModeIsOnFeedback = new BoolFeedback(PrivacyModeIsOnFeedbackFunc); + VolumeLevelFeedback = new IntFeedback(VolumeLevelFeedbackFunc); + MuteFeedback = new BoolFeedback(MuteFeedbackFunc); + SharingSourceFeedback = new StringFeedback(SharingSourceFeedbackFunc); + + InputPorts = new RoutingPortCollection(); + + ActiveCallCountFeedback.OutputChange += new EventHandler(ActiveCallCountFeedback_OutputChange); + + ActiveCalls = new List(); + } + + /// + /// + /// + /// + /// + void ActiveCallCountFeedback_OutputChange(object sender, EventArgs e) + { + if (UsageTracker != null) + { + if (IsInCall) + UsageTracker.StartDeviceUsage(); + else + UsageTracker.EndDeviceUsage(); + } + } + #region IHasDialer Members + + public abstract void Dial(string s); +<<<<<<< HEAD + public abstract void EndCall(string s); +======= + public void EndCall(object activeCall) + { + + } + public abstract void EndCall(CodecActiveCallItem activeCall); + public abstract void EndAllCalls(); +>>>>>>> origin/feature/cisco-spark-2 + public abstract void AcceptCall(); + public abstract void RejectCall(); + public abstract void SendDtmf(string s); + + #endregion + + public virtual List Feedbacks + { + get + { + return new List + { + IncomingCallFeedback, + PrivacyModeIsOnFeedback, + SharingSourceFeedback + }; + } + } + + public abstract void ExecuteSwitch(object selector); + + #region ICodecAudio Members + + public abstract void PrivacyModeOn(); + public abstract void PrivacyModeOff(); + public abstract void PrivacyModeToggle(); + public BoolFeedback PrivacyModeIsOnFeedback { get; private set; } + + + public BoolFeedback MuteFeedback { get; private set; } + + public abstract void MuteOff(); + + public abstract void MuteOn(); + + public abstract void SetVolume(ushort level); + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public abstract void MuteToggle(); + + public abstract void VolumeDown(bool pressRelease); + + + public abstract void VolumeUp(bool pressRelease); + + #endregion + + #region IHasSharing Members + + public abstract void StartSharing(); + public abstract void StopSharing(); + + public StringFeedback SharingSourceFeedback { get; private set; } + + #endregion + + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ResponseObjects.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ResponseObjects.cs new file mode 100644 index 00000000..3a78c5e0 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ResponseObjects.cs @@ -0,0 +1,1184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom +{ + public enum eZoomRoomResponseType + { + zEvent, + zStatus, + zConfiguration, + zCommand + } + + public abstract class NotifiableObject : INotifyPropertyChanged + { + #region INotifyPropertyChanged Members + + public event PropertyChangedEventHandler PropertyChanged; + + protected void NotifyPropertyChanged(string propertyName) + { + if (PropertyChanged != null) + { + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + } + } + + #endregion + } + + /// + /// Used to track the current status of a ZoomRoom + /// + public class ZoomRoomStatus + { + public zStatus.Login Login { get; set; } + public zStatus.SystemUnit SystemUnit { get; set; } + public zStatus.Phonebook Phonebook { get; set; } + public zStatus.Call Call { get; set; } + public zStatus.Capabilities Capabilities { get; set; } + public zStatus.Sharing Sharing { get; set; } + public zStatus.NumberOfScreens NumberOfScreens { get; set; } + public zStatus.Layout Layout { get; set; } + public zStatus.Video Video { get; set; } + public zStatus.CameraShare CameraShare { get; set; } + public List AudioInputs { get; set; } + public List AudioOuputs { get; set; } + public List Cameras { get; set; } + + public ZoomRoomStatus() + { + Login = new zStatus.Login(); + SystemUnit = new zStatus.SystemUnit(); + Phonebook = new zStatus.Phonebook(); + Call = new zStatus.Call(); + Capabilities = new zStatus.Capabilities(); + Sharing = new zStatus.Sharing(); + NumberOfScreens = new zStatus.NumberOfScreens(); + Layout = new zStatus.Layout(); + Video = new zStatus.Video(); + CameraShare = new zStatus.CameraShare(); + AudioInputs = new List(); + AudioOuputs = new List(); + Cameras = new List(); + } + } + + /// + /// Used to track the current configuration of a ZoomRoom + /// + public class ZoomRoomConfiguration + { + public zConfiguration.Call Call { get; set; } + public zConfiguration.Audio Audio { get; set; } + public zConfiguration.Video Video { get; set; } + public zConfiguration.Client Client { get; set; } + + public ZoomRoomConfiguration() + { + Call = new zConfiguration.Call(); + Audio = new zConfiguration.Audio(); + Video = new zConfiguration.Video(); + Client = new zConfiguration.Client(); + } + } + + /// + /// Represents a response from a ZoomRoom system + /// + public class Response + { + public Status Status { get; set; } + public bool Sync { get; set; } + [JsonProperty("topKey")] + public string TopKey { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + + public Response() + { + Status = new Status(); + } + } + + public class Status + { + [JsonProperty("message")] + public string Message { get; set; } + [JsonProperty("state")] + public string State { get; set; } + } + + + /// + /// zStatus class stucture + /// + public class zStatus + { + public class Login + { + [JsonProperty("ZAAPI Release")] + public string ZAAPIRelease { get; set; } + [JsonProperty("Zoom Room Release")] + public string ZoomRoomRelease { get; set; } + } + + public class SystemUnit + { + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("login_type")] + public string LoginType { get; set; } + [JsonProperty("meeting_number")] + public string MeetingNumber { get; set; } + [JsonProperty("platform")] + public string Platform { get; set; } + [JsonProperty("room_info")] + public RoomInfo RoomInfo { get; set; } + [JsonProperty("room_version")] + public string RoomVersion { get; set; } + + public SystemUnit() + { + RoomInfo = new RoomInfo(); + } + } + + public class RoomInfo + { + [JsonProperty("account_email")] + public string AccountEmail { get; set; } + [JsonProperty("display_version")] + public string DisplayVersion { get; set; } + [JsonProperty("is_auto_answer_enabled")] + public bool AutoAnswerIsEnabled { get; set; } + [JsonProperty("is_auto_answer_selected")] + public bool AutoAnswerIsSelected { get; set; } + [JsonProperty("room_name")] + public string RoomName { get; set; } + } + + public class CloudPbxInfo + { + [JsonProperty("company_number")] + public string CompanyNumber { get; set; } + [JsonProperty("extension")] + public string Extension { get; set; } + [JsonProperty("isValid")] + public bool IsValid { get; set; } + } + + public enum ePresence + { + PRESENCE_OFFLINE, + PRESENCE_ONLINE, + PRESENCE_AWAY, + PRESENCE_BUSY, + PRESENCE_DND + } + + public class Contact + { + [JsonProperty("avatarURL")] + public string AvatarURL { get; set; } + [JsonProperty("cloud_pbx_info")] + public CloudPbxInfo CloudPbxInfo { get; set; } + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("firstName")] + public string FirstName { get; set; } + [JsonProperty("index")] + public int Index { get; set; } + [JsonProperty("isLegacy")] + public bool IsLegacy { get; set; } + [JsonProperty("isZoomRoom")] + public bool IsZoomRoom { get; set; } + [JsonProperty("jid")] + public string Jid { get; set; } + [JsonProperty("lastName")] + public string LastName { get; set; } + [JsonProperty("onDesktop")] + public bool OnDesktop { get; set; } + [JsonProperty("onMobile")] + public bool OnMobile { get; set; } + [JsonProperty("phoneNumber")] + public string PhoneNumber { get; set; } + [JsonProperty("presence")] + public ePresence Presence { get; set; } + [JsonProperty("presence_status")] + public int PresenceStatus { get; set; } + [JsonProperty("screenName")] + public string ScreenName { get; set; } + [JsonProperty("sip_phone_number")] + public string SipPhoneNumber { get; set; } + + + public Contact() + { + CloudPbxInfo = new CloudPbxInfo(); + } + } + + /// + /// Used to be able to inplement IInvitableContact on DirectoryContact + /// + public class ZoomDirectoryContact : DirectoryContact, IInvitableContact + { + + } + + public class Phonebook + { + [JsonProperty("Contacts")] + public List Contacts { get; set; } + + public Phonebook() + { + Contacts = new List(); + } + + /// + /// Converts from zStatus.Contact types to generic directory items + /// + /// + public static CodecDirectory ConvertZoomContactsToGeneric(List zoomContacts) + { + var directory = new Codec.CodecDirectory(); + + var folders = new List(); + + var roomFolder = new DirectoryFolder(); + + var contactFolder = new DirectoryFolder(); + + var contacts = new List(); + + // Check if there are any zoom rooms + var zoomRooms = zoomContacts.FindAll(c => c.IsZoomRoom); + + if (zoomRooms.Count > 0) + { + // If so, setup a rooms and contacts folder and add them. + roomFolder.Name = "Rooms"; + roomFolder.FolderId = "rooms"; + + contactFolder.Name = "Contacts"; + contactFolder.FolderId = "contacts"; + + folders.Add(roomFolder); + folders.Add(contactFolder); + + directory.AddFoldersToDirectory(folders); + } + + try + { + if (zoomContacts.Count > 0) + { + foreach (Contact c in zoomContacts) + { + var contact = new ZoomDirectoryContact(); + + contact.Name = c.ScreenName; + contact.ContactId = c.Jid; + + if (folders.Count > 0) + { + if (c.IsZoomRoom) + contact.FolderId = roomFolder.FolderId; + else + contact.FolderId = contactFolder.FolderId; + } + + contacts.Add(contact); + } + + directory.AddContactsToDirectory(contacts); + } + } + catch (Exception e) + { + Debug.Console(1, "Error converting Zoom Phonebook results to generic: {0}", e); + } + + return directory; + } + } + + public enum eCallStatus + { + UNKNOWN, + NOT_IN_MEETING, + CONNECTING_MEETING, + IN_MEETING, + LOGGED_OUT + } + + public class ClosedCaption + { + public bool Available { get; set; } + } + + public class Call : NotifiableObject + { + private eCallStatus _status; + private List _participants; + + public bool IsInCall; + + public eCallStatus Status + { + get + { + return _status; + } + set + { + if (value != _status) + { + _status = value; + IsInCall = _status == eCallStatus.IN_MEETING || _status == eCallStatus.CONNECTING_MEETING; + NotifyPropertyChanged("Status"); + } + } + } + public ClosedCaption ClosedCaption { get; set; } + public List Participants + { + get + { + return _participants; + } + set + { + _participants = value; + NotifyPropertyChanged("Participants"); + } + } + public zEvent.SharingState Sharing { get; set; } + + public CallRecordInfo CallRecordInfo { get; set; } + + public zCommand.InfoResult Info { get; set; } + + public Call() + { + ClosedCaption = new ClosedCaption(); + Participants = new List(); + Sharing = new zEvent.SharingState(); + CallRecordInfo = new CallRecordInfo(); + Info = new zCommand.InfoResult(); + } + } + + public class Capabilities + { + public bool aec_Setting_Stored_In_ZR { get; set; } + public bool can_Dtmf_For_Invite_By_Phone { get; set; } + public bool can_Mute_On_Entry { get; set; } + public bool can_Ringing_In_Pstn_Call { get; set; } + public bool can_Switch_To_Specific_Camera { get; set; } + public bool is_Airhost_Disabled { get; set; } + public bool pstn_Call_In_Local_resentation { get; set; } + public bool support_Claim_Host { get; set; } + public bool support_Out_Room_Display { get; set; } + public bool support_Pin_And_Spotlight { get; set; } + public bool supports_Audio_Checkup { get; set; } + public bool supports_CheckIn { get; set; } + public bool supports_Cloud_PBX { get; set; } + public bool supports_Encrypted_Connection { get; set; } + public bool supports_Expel_User_Permanently { get; set; } + public bool supports_H323_DTMF { get; set; } + public bool supports_Hdmi_Cec_Control { get; set; } + public bool supports_Highly_Reverberant_Room { get; set; } + public bool supports_Loading_Contacts_Dynamically { get; set; } + public bool supports_Loading_Participants_Dynamically { get; set; } + public bool supports_Mic_Record_Test { get; set; } + public bool supports_Multi_Share { get; set; } + public bool supports_ShareCamera { get; set; } + public bool supports_Share_For_Floating_And_Content_Only { get; set; } + public bool supports_Sip_Call_out { get; set; } + public bool supports_Software_Audio_Processing { get; set; } + public bool supports_Web_Settings_Push { get; set; } + } + + public class Sharing : NotifiableObject + { + private string _dispState; + private string _password; + + public string directPresentationPairingCode { get; set; } + /// + /// Laptop client sharing key + /// + public string directPresentationSharingKey { get; set; } + public string dispState + { + get + { + return _dispState; + } + set + { + if (value != _dispState) + { + _dispState = value; + NotifyPropertyChanged("dispState"); + } + } + } + public bool isAirHostClientConnected { get; set; } + public bool isBlackMagicConnected { get; set; } + public bool isBlackMagicDataAvailable { get; set; } + public bool isDirectPresentationConnected { get; set; } + public bool isSharingBlackMagic { get; set; } + /// + /// IOS Airplay code + /// + public string password + { + get + { + return _password; + } + set + { + if (value != _password) + { + _password = value; + NotifyPropertyChanged("password"); + } + } + } + public string serverName { get; set; } + public string wifiName { get; set; } + } + + public class NumberOfScreens + { + [JsonProperty("NumberOfCECScreens")] + public int NumOfCECScreens { get; set; } + [JsonProperty("NumberOfScreens")] + public int NumOfScreens { get; set; } + } + + /// + /// AudioInputLine/AudioOutputLine/VideoCameraLine list item + /// + public class AudioVideoInputOutputLineItem + { + public string Alias { get; set; } + public string Name { get; set; } + public bool Selected { get; set; } + public bool combinedDevice { get; set; } + public string id { get; set; } + public bool manuallySelected { get; set; } + public int numberOfCombinedDevices { get; set; } + public int ptzComId { get; set; } + } + + public class Video + { + public bool Optimizable { get; set; } + } + + public class CameraShare : NotifiableObject + { + private bool _canControlCamera; + private bool _isSharing; + + [JsonProperty("can_Control_Camera")] + public bool CanControlCamera + { + get + { + return _canControlCamera; + } + set + { + if (value != _canControlCamera) + { + _canControlCamera = value; + NotifyPropertyChanged("CanControlCamera"); + } + } + } + public string id { get; set; } + public bool is_Mirrored { get; set; } + [JsonProperty("is_Sharing")] + public bool IsSharing + { + get + { + return _isSharing; + } + set + { + if (value != _isSharing) + { + _isSharing = value; + NotifyPropertyChanged("IsSharing"); + } + } + } + public int pan_Tilt_Speed { get; set; } + + } + + public class Layout + { + public bool can_Adjust_Floating_Video { get; set; } + public bool can_Switch_Floating_Share_Content { get; set; } + public bool can_Switch_Share_On_All_Screens { get; set; } + public bool can_Switch_Speaker_View { get; set; } + public bool can_Switch_Wall_View { get; set; } + public bool is_In_First_Page { get; set; } + public bool is_In_Last_Page { get; set; } + public bool is_supported { get; set; } + public int video_Count_In_Current_Page { get; set; } + public string video_type { get; set; } + } + + public class CallRecordInfo + { + public bool canRecord { get; set; } + public bool emailRequired { get; set; } + public bool amIRecording { get; set; } + public bool meetingIsBeingRecorded { get; set; } + } + } + + /// + /// zEvent Class Structure + /// + public class zEvent + { + public class NeedWaitForHost + { + public bool Wait { get; set; } + } + + public class IncomingCallIndication + { + public string callerJID { get; set; } + public string calleeJID { get; set; } + public string meetingID { get; set; } + public string password { get; set; } + public string meetingOption { get; set; } + public int MeetingNumber { get; set; } + public string callerName { get; set; } + public string avatarURL { get; set; } + public int lifeTime { get; set; } + public bool accepted { get; set; } + } + + public class CallConnectError + { + public int error_code { get; set; } + public string error_message { get; set; } + } + + public class CallDisconnect + { + public bool Successful + { + get + { + return success == "on"; + } + } + + public string success { get; set; } + } + + public class Layout + { + public bool Sharethumb { get; set; } + } + + public class Call + { + public Layout Layout { get; set; } + } + + public class Client + { + public Call Call { get; set; } + } + + public enum eSharingState + { + None, + Connecting, + Sending, + Receiving, + Send_Receiving + } + + public class SharingState : NotifiableObject + { + private bool _paused; + private eSharingState _state; + + public bool IsSharing; + + [JsonProperty("paused")] + public bool Paused + { + get + { + return _paused; + } + set + { + if (value != _paused) + { + _paused = value; + NotifyPropertyChanged("Paused"); + } + } + } + [JsonProperty("state")] + public eSharingState State + { + get + { + return _state; + } + set + { + if (value != _state) + { + _state = value; + IsSharing = _state == eSharingState.Sending; + NotifyPropertyChanged("State"); + } + } + } + } + + public class PinStatusOfScreenNotification + { + [JsonProperty("can_be_pinned")] + public bool CanBePinned { get; set; } + [JsonProperty("can_pin_share")] + public bool CanPinShare { get; set; } + [JsonProperty("pinned_share_source_id")] + public int PinnedShareSourceId { get; set; } + [JsonProperty("pinned_user_id")] + public int PinnedUserId { get; set; } + [JsonProperty("screen_index")] + public int ScreenIndex { get; set; } + [JsonProperty("screen_layout")] + public int ScreenLayout { get; set; } + [JsonProperty("share_source_type")] + public int ShareSourceType { get; set; } + [JsonProperty("why_cannot_pin_share")] + public string WhyCannotPinShare { get; set; } + } + } + + /// + /// zConfiguration class structure + /// + public class zConfiguration + { + public class Sharing + { + [JsonProperty("optimize_video_sharing")] + public bool OptimizeVideoSharing { get; set; } + } + + public class Camera + { + public bool Mute { get; set; } + } + + public class Microphone : NotifiableObject + { + private bool _mute; + + public bool Mute + { + get + { + return _mute; + } + set + { + if(value != _mute) + { + _mute = value; + NotifyPropertyChanged("Mute"); + } + } + } + } + + public enum eLayoutStyle + { + Gallery, + Speaker, + Strip, + ShareAll + } + + public enum eLayoutSize + { + Off, + Size1, + Size2, + Size3, + Strip + } + + public enum eLayoutPosition + { + Center, + Up, + Right, + UpRight, + Down, + DownRight, + Left, + UpLeft, + DownLeft + } + + public class Layout + { + public bool ShareThumb { get; set; } + public eLayoutStyle Style { get; set; } + public eLayoutSize Size { get; set; } + public eLayoutPosition Position { get; set; } + } + + public class Lock + { + public bool Enable { get; set; } + } + + public class ClosedCaption + { + public bool Visible { get; set; } + public int FontSize { get; set; } + } + + public class MuteUserOnEntry + { + public bool Enable { get; set; } + } + + public class Call + { + public Sharing Sharing { get; set; } + public Camera Camera { get; set; } + public Microphone Microphone { get; set; } + public Layout Layout { get; set; } + public Lock Lock { get; set; } + public MuteUserOnEntry MuteUserOnEntry { get; set; } + public ClosedCaption ClosedCaption { get; set; } + + + public Call() + { + Sharing = new Sharing(); + Camera = new Camera(); + Microphone = new Microphone(); + Layout = new Layout(); + Lock = new Lock(); + MuteUserOnEntry = new MuteUserOnEntry(); + ClosedCaption = new ClosedCaption(); + } + } + + public class Audio + { + public Input Input { get; set; } + public Output Output { get; set; } + + public Audio() + { + Input = new Input(); + Output = new Output(); + } + } + + public class Input : Output + { + [JsonProperty("reduce_reverb")] + public bool ReduceReverb { get; set; } + } + + public class Output : NotifiableObject + { + private int _volume; + + [JsonProperty("volume")] + public int Volume + { + get + { + return this._volume; + } + set + { + if (value != _volume) + { + this._volume = value; + NotifyPropertyChanged("Volume"); + } + } + } + [JsonProperty("selectedId")] + public string SelectedId { get; set; } + [JsonProperty("is_sap_disabled")] + public bool IsSapDisabled { get; set; } + } + + public class Video : NotifiableObject + { + private bool _hideConfSelfVideo; + + [JsonProperty("hide_conf_self_video")] + public bool HideConfSelfVideo + { + get + { + return _hideConfSelfVideo; + } + set + { + if (value != _hideConfSelfVideo) + { + _hideConfSelfVideo = value; + NotifyPropertyChanged("HideConfSelfVideo"); + } + } + } + + public VideoCamera Camera { get; set; } + + public Video() + { + Camera = new VideoCamera(); + } + } + + public class VideoCamera : NotifiableObject + { + private string _selectedId; + + [JsonProperty("selectedId")] + public string SelectedId { + get + { + return _selectedId; + } + set + { + if (value != _selectedId) + { + _selectedId = value; + NotifyPropertyChanged("SelectedId"); + } + } + + } + public bool Mirror { get; set; } + } + + public class Client + { + public string appVersion { get; set; } + public string deviceSystem { get; set; } + } + + } + + /// + /// zCommand class structure + /// + public class zCommand + { + public partial class BookingsListResult + { + [JsonProperty("accessRole")] + public string AccessRole { get; set; } + [JsonProperty("calendarChangeKey")] + public string CalendarChangeKey { get; set; } + [JsonProperty("calendarID")] + public string CalendarId { get; set; } + [JsonProperty("checkIn")] + public bool CheckIn { get; set; } + [JsonProperty("creatorEmail")] + public string CreatorEmail { get; set; } + [JsonProperty("creatorName")] + public string CreatorName { get; set; } + [JsonProperty("endTime")] + public DateTime EndTime { get; set; } + [JsonProperty("hostName")] + public string HostName { get; set; } + [JsonProperty("isInstantMeeting")] + public bool IsInstantMeeting { get; set; } + [JsonProperty("isPrivate")] + public bool IsPrivate { get; set; } + [JsonProperty("location")] + public string Location { get; set; } + [JsonProperty("meetingName")] + public string MeetingName { get; set; } + [JsonProperty("meetingNumber")] + public string MeetingNumber { get; set; } + [JsonProperty("scheduledFrom")] + public string ScheduledFrom { get; set; } + [JsonProperty("startTime")] + public DateTime StartTime { get; set; } + [JsonProperty("third_party")] + public ThirdParty ThirdParty { get; set; } + } + + /// + /// Extracts the necessary meeting values from the Cisco bookings response ans converts them to the generic class + /// + /// + /// + public static List GetGenericMeetingsFromBookingResult(List bookings) + { + var meetings = new List(); + + if (Debug.Level > 0) + { + Debug.Console(1, "Meetings List:\n"); + } + + foreach (var b in bookings) + { + var meeting = new Meeting(); + + if (b.MeetingNumber != null) + meeting.Id = b.MeetingNumber; + if (b.CreatorName != null) + meeting.Organizer = b.CreatorName; + if (b.MeetingName != null) + meeting.Title = b.MeetingName; + //if (b.Agenda != null) + // meeting.Agenda = b.Agenda.Value; + if (b.StartTime != null) + meeting.StartTime = b.StartTime; + if (b.EndTime != null) + meeting.EndTime = b.EndTime; + if (b.IsPrivate != null) + meeting.Privacy = b.IsPrivate ? eMeetingPrivacy.Private : eMeetingPrivacy.Public; + + // No meeting.Calls data exists for Zoom Rooms. Leaving out for now. + + meetings.Add(meeting); + + if (Debug.Level > 0) + { + Debug.Console(1, "Title: {0}, ID: {1}, Organizer: {2}", meeting.Title, meeting.Id, meeting.Organizer); + Debug.Console(1, " Start Time: {0}, End Time: {1}, Duration: {2}", meeting.StartTime, meeting.EndTime, meeting.Duration); + Debug.Console(1, " Joinable: {0}\n", meeting.Joinable); + } + } + + meetings.OrderBy(m => m.StartTime); + + return meetings; + } + + public class HandStatus + { + [JsonProperty("is_raise_hand")] + public bool IsRaiseHand { get; set; } + [JsonProperty("optimize_vis_validideo_sharing")] + public string IsValid { get; set; } + [JsonProperty("time_stamp")] + public string TimeStamp { get; set; } + } + + public class ListParticipant + { + [JsonProperty("audio_status state")] + public string AudioStatusState { get; set; } + [JsonProperty("audio_status type")] + public string AudioStatusType { get; set; } + [JsonProperty("avatar_url")] + public string AvatarUrl { get; set; } + [JsonProperty("camera_status am_i_controlling")] + public bool CameraStatusAmIControlling { get; set; } + [JsonProperty("camera_status can_i_request_control")] + public bool CameraStatusCanIRequestConrol { get; set; } + [JsonProperty("camera_status can_move_camera")] + public bool CameraStatusCanMoveCamera { get; set; } + [JsonProperty("camera_status can_switch_camera")] + public bool CameraStatusCanSwitchCamera { get; set; } + [JsonProperty("camera_status can_zoom_camera")] + public bool CameraStatusCanZoomCamera { get; set; } + [JsonProperty("can_edit_closed_caption")] + public bool CanEditClosedCaption { get; set; } + [JsonProperty("can_record")] + public bool CanRecord { get; set; } + [JsonProperty("event")] + public string Event { get; set; } + [JsonProperty("hand_status")] + public HandStatus HandStatus { get; set; } + [JsonProperty("isCohost")] + public bool IsCohost { get; set; } + [JsonProperty("is_client_support_closed_caption")] + public bool IsClientSupportClosedCaption { get; set; } + [JsonProperty("is_client_support_coHost")] + public bool IsClientSupportCoHost { get; set; } + [JsonProperty("is_host")] + public bool IsHost { get; set; } + [JsonProperty("is_myself")] + public bool IsMyself { get; set; } + [JsonProperty("is_recording")] + public bool IsRecording { get; set; } + [JsonProperty("is_video_can_mute_byHost")] + public bool IsVideoCanMuteByHost { get; set; } + [JsonProperty("is_video_can_unmute_byHost")] + public bool IsVideoCanUnmuteByHost { get; set; } + [JsonProperty("local_recording_disabled")] + public bool LocalRecordingDisabled { get; set; } + [JsonProperty("user_id")] + public int UserId { get; set; } + [JsonProperty("user_name")] + public string UserName { get; set; } + [JsonProperty("user_type")] + public string UserType { get; set; } + [JsonProperty("video_status has_source")] + public bool VideoStatusHasSource { get; set; } + [JsonProperty("video_status is_receiving")] + public bool VideoStatusIsReceiving { get; set; } + [JsonProperty("video_status is_sending")] + public bool VideoStatusIsSending { get; set; } + + public ListParticipant() + { + HandStatus = new HandStatus(); + } + } + + public class CallinCountryList + { + public int code { get; set; } + public string display_number { get; set; } + public string id { get; set; } + public string name { get; set; } + public string number { get; set; } + } + + public class CalloutCountryList + { + public int code { get; set; } + public string display_number { get; set; } + public string id { get; set; } + public string name { get; set; } + public string number { get; set; } + } + + public class TollFreeCallinList + { + public int code { get; set; } + public string display_number { get; set; } + public string id { get; set; } + public string name { get; set; } + public string number { get; set; } + } + + public class Info + { + public List callin_country_list { get; set; } + public List callout_country_list { get; set; } + public List toll_free_callin_list { get; set; } + } + + public class ThirdParty + { + public string h323_address { get; set; } + public string meeting_number { get; set; } + public string service_provider { get; set; } + public string sip_address { get; set; } + } + + public class MeetingListItem + { + public string accessRole { get; set; } + public string calendarChangeKey { get; set; } + public string calendarID { get; set; } + public bool checkIn { get; set; } + public string creatorEmail { get; set; } + public string creatorName { get; set; } + public string endTime { get; set; } + public string hostName { get; set; } + public bool isInstantMeeting { get; set; } + public bool isPrivate { get; set; } + public string location { get; set; } + public string meetingName { get; set; } + public string meetingNumber { get; set; } + public string scheduledFrom { get; set; } + public string startTime { get; set; } + public ThirdParty third_party { get; set; } + + public MeetingListItem() + { + third_party = new ThirdParty(); + } + } + + public class InfoResult + { + public Info Info { get; set; } + public bool am_i_original_host { get; set; } + public string default_callin_country { get; set; } + public string dialIn { get; set; } + public string international_url { get; set; } + public string invite_email_content { get; set; } + public string invite_email_subject { get; set; } + public bool is_callin_country_list_available { get; set; } + public bool is_calling_room_system_enabled { get; set; } + public bool is_toll_free_callin_list_available { get; set; } + public bool is_view_only { get; set; } + public bool is_waiting_room { get; set; } + public bool is_webinar { get; set; } + public string meeting_id { get; set; } + public MeetingListItem meeting_list_item { get; set; } + public string meeting_password { get; set; } + public string meeting_type { get; set; } + public int my_userid { get; set; } + public int participant_id { get; set; } + public string real_meeting_id { get; set; } + public string schedule_option { get; set; } + public string schedule_option2 { get; set; } + public string support_callout_type { get; set; } + public string toll_free_number { get; set; } + public string user_type { get; set; } + + public InfoResult() + { + Info = new Info(); + meeting_list_item = new MeetingListItem(); + } + } + + public class Phonebook + { + public List Contacts { get; set; } + public int Limit { get; set; } + public int Offset { get; set; } + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoom.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoom.cs new file mode 100644 index 00000000..e1fb5ee4 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoom.cs @@ -0,0 +1,1679 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.CrestronThread; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.Occupancy; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom +{ + public class ZoomRoom : VideoCodecBase, IHasCodecSelfView, IHasDirectory, ICommunicationMonitor, IRouting, IHasScheduleAwareness, IHasCodecCameras + { + public CommunicationGather PortGather { get; private set; } + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + private CrestronQueue ReceiveQueue; + + private Thread ReceiveThread; + + string Delimiter = "\x0D\x0A"; + + private ZoomRoomSyncState SyncState; + + public ZoomRoomStatus Status { get; private set; } + + public ZoomRoomConfiguration Configuration { get; private set; } + + private StringBuilder JsonMessage; + + private bool JsonFeedbackMessageIsIncoming; + private uint JsonCurlyBraceCounter = 0; + + public bool CommDebuggingIsOn; + + CTimer LoginMessageReceivedTimer; + CTimer RetryConnectionTimer; + + /// + /// Gets and returns the scaled volume of the codec + /// + protected override Func VolumeLevelFeedbackFunc + { + get + { + return () => CrestronEnvironment.ScaleWithLimits(Configuration.Audio.Output.Volume, 100, 0, 65535, 0); + } + } + + protected override Func PrivacyModeIsOnFeedbackFunc + { + get + { + return () => Configuration.Call.Microphone.Mute; + } + } + + protected override Func StandbyIsOnFeedbackFunc + { + get + { + return () => false; + } + } + + protected override Func SharingSourceFeedbackFunc + { + get + { + return () => Status.Sharing.dispState; + } + } + + protected override Func SharingContentIsOnFeedbackFunc + { + get + { + return () => Status.Call.Sharing.IsSharing; + } + } + + protected Func FarEndIsSharingContentFeedbackFunc + { + get + { + return () => false; + } + } + + protected override Func MuteFeedbackFunc + { + get + { + return () => Configuration.Audio.Output.Volume == 0; + } + } + + //protected Func RoomIsOccupiedFeedbackFunc + //{ + // get + // { + // return () => false; + // } + //} + + //protected Func PeopleCountFeedbackFunc + //{ + // get + // { + // return () => 0; + // } + //} + + protected Func SelfViewIsOnFeedbackFunc + { + get + { + return () => !Configuration.Video.HideConfSelfVideo; + } + } + + protected Func SelfviewPipPositionFeedbackFunc + { + get + { + return () => ""; + } + } + + protected Func LocalLayoutFeedbackFunc + { + get + { + return () => ""; + } + } + + protected Func LocalLayoutIsProminentFeedbackFunc + { + get + { + return () => false; + } + } + + + public RoutingInputPort CodecOsdIn { get; private set; } + public RoutingOutputPort Output1 { get; private set; } + + uint DefaultMeetingDurationMin = 30; + + int PreviousVolumeLevel = 0; + + public ZoomRoom(DeviceConfig config, IBasicCommunication comm) + : base(config) + { + var props = JsonConvert.DeserializeObject(config.Properties.ToString()); + + // The queue that will collect the repsonses in the order they are received + ReceiveQueue = new CrestronQueue(25); + + // The thread responsible for dequeuing and processing the messages + ReceiveThread = new Thread((o) => ProcessQueue(), null); + + Communication = comm; + + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 30000, 120000, 300000, "zStatus SystemUnit\r"); + } + + DeviceManager.AddDevice(CommunicationMonitor); + + Status = new ZoomRoomStatus(); + + Configuration = new ZoomRoomConfiguration(); + + CodecInfo = new ZoomRoomInfo(Status, Configuration); + + SyncState = new ZoomRoomSyncState(Key + "--Sync", this); + + SyncState.InitialSyncCompleted += new EventHandler(SyncState_InitialSyncCompleted); + + PhonebookSyncState = new CodecPhonebookSyncState(Key + "--PhonebookSync"); + + PortGather = new CommunicationGather(Communication, "\x0A"); + PortGather.IncludeDelimiter = true; + PortGather.LineReceived += this.Port_LineReceived; + + CodecOsdIn = new RoutingInputPort(RoutingPortNames.CodecOsd, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(StopSharing), this); + + Output1 = new RoutingOutputPort(RoutingPortNames.AnyVideoOut, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, null, this); + + SelfviewIsOnFeedback = new BoolFeedback(SelfViewIsOnFeedbackFunc); + + CodecSchedule = new CodecScheduleAwareness(); + + SetUpFeedbackActions(); + + Cameras = new List(); + + SetUpDirectory(); + } + + void SyncState_InitialSyncCompleted(object sender, EventArgs e) + { + SetUpRouting(); + + SetIsReady(); + } + + + /// + /// Subscribes to the PropertyChanged events on the state objects and fires the corresponding feedbacks. + /// + void SetUpFeedbackActions() + { + Configuration.Audio.Output.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( + (o, a) => + { + if (a.PropertyName == "Volume") + { + VolumeLevelFeedback.FireUpdate(); + MuteFeedback.FireUpdate(); + } + }); + + Configuration.Call.Microphone.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( + (o, a) => + { + if (a.PropertyName == "Mute") + { + PrivacyModeIsOnFeedback.FireUpdate(); + } + }); + + Configuration.Video.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( + (o, a) => + { + if (a.PropertyName == "HideConfSelfVideo") + { + SelfviewIsOnFeedback.FireUpdate(); + } + }); + Configuration.Video.Camera.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( + (o, a) => + { + if (a.PropertyName == "SelectedId") + { + SelectCamera(Configuration.Video.Camera.SelectedId); // this will in turn fire the affected feedbacks + } + }); + + Status.Call.Sharing.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( + (o, a) => + { + if (a.PropertyName == "State") + { + SharingContentIsOnFeedback.FireUpdate(); + } + }); + + Status.Sharing.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( + (o, a) => + { + if (a.PropertyName == "dispState") + { + SharingSourceFeedback.FireUpdate(); + } + else if (a.PropertyName == "password") + { + //TODO: Fire Sharing Password Update + } + }); + } + + void SetUpDirectory() + { + DirectoryRoot = new CodecDirectory(); + + DirectoryBrowseHistory = new List(); + + CurrentDirectoryResultIsNotDirectoryRoot = new BoolFeedback(() => DirectoryBrowseHistory.Count > 0); + + CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate(); + } + + void SetUpRouting() + { + // Set up input ports + CreateOsdSource(); + InputPorts.Add(CodecOsdIn); + + // Set up output ports + OutputPorts.Add(Output1); + } + + /// + /// 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); + + //foreach(var input in Status.Video. + } + + /// + /// 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((s) => SendText("zCommand Phonebook List Offset: 0 Limit: 512"), "GetZoomRoomContacts", "Triggers a refresh of the codec phonebook", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand((s) => GetBookings(), "GetZoomRoomBookings", "Triggers a refresh of the booking data for today", ConsoleAccessLevelEnum.AccessOperator); + + var socket = Communication as ISocketStatus; + if (socket != null) + { + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + + // TODO: Turn this off when done initial development + CommDebuggingIsOn = true; + + Communication.Connect(); + + CommunicationMonitor.Start(); + + return base.CustomActivate(); + } + + 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) + { + + } + else + { + SyncState.CodecDisconnected(); + PhonebookSyncState.CodecDisconnected(); + } + } + + public void SendText(string command) + { + if (CommDebuggingIsOn) + Debug.Console(1, this, "Sending: '{0}'", command); + + Communication.SendText(command + Delimiter); + } + + /// + /// Gathers responses and enqueues them. + /// + /// + /// + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + //if (CommDebuggingIsOn) + // Debug.Console(1, this, "Gathered: '{0}'", args.Text); + + ReceiveQueue.Enqueue(args.Text); + + // If the receive thread has for some reason stopped, this will restart it + if (ReceiveThread.ThreadState != Thread.eThreadStates.ThreadRunning) + ReceiveThread.Start(); + } + + + /// + /// Runs in it's own thread to dequeue messages in the order they were received to be processed + /// + /// + object ProcessQueue() + { + try + { + while (true) + { + var message = ReceiveQueue.Dequeue(); + + ProcessMessage(message); + } + } + catch (Exception e) + { + Debug.Console(1, this, "Error Processing Queue: {0}", e); + } + + return null; + } + + + /// + /// Queues the initial queries to be sent upon connection + /// + void SetUpSyncQueries() + { + // zStatus + SyncState.AddQueryToQueue("zStatus Call Status"); + SyncState.AddQueryToQueue("zStatus Audio Input Line"); + SyncState.AddQueryToQueue("zStatus Audio Output Line"); + SyncState.AddQueryToQueue("zStatus Video Camera Line"); + SyncState.AddQueryToQueue("zStatus Video Optimizable"); + SyncState.AddQueryToQueue("zStatus Capabilities"); + SyncState.AddQueryToQueue("zStatus Sharing"); + SyncState.AddQueryToQueue("zStatus CameraShare"); + SyncState.AddQueryToQueue("zStatus Call Layout"); + SyncState.AddQueryToQueue("zStatus Call ClosedCaption Available"); + SyncState.AddQueryToQueue("zStatus NumberOfScreens"); + + // zConfiguration + + SyncState.AddQueryToQueue("zConfiguration Call Sharing optimize_video_sharing"); + SyncState.AddQueryToQueue("zConfiguration Call Microphone Mute"); + SyncState.AddQueryToQueue("zConfiguration Call Camera Mute"); + SyncState.AddQueryToQueue("zConfiguration Audio Input SelectedId"); + SyncState.AddQueryToQueue("zConfiguration Audio Input is_sap_disabled"); + SyncState.AddQueryToQueue("zConfiguration Audio Input reduce_reverb"); + SyncState.AddQueryToQueue("zConfiguration Audio Input volume"); + SyncState.AddQueryToQueue("zConfiguration Audio Output selectedId"); + SyncState.AddQueryToQueue("zConfiguration Audio Output volume"); + SyncState.AddQueryToQueue("zConfiguration Video hide_conf_self_video"); + SyncState.AddQueryToQueue("zConfiguration Video Camera selectedId"); + SyncState.AddQueryToQueue("zConfiguration Video Camera Mirror"); + SyncState.AddQueryToQueue("zConfiguration Client appVersion"); + SyncState.AddQueryToQueue("zConfiguration Client deviceSystem"); + SyncState.AddQueryToQueue("zConfiguration Call Layout ShareThumb"); + SyncState.AddQueryToQueue("zConfiguration Call Layout Style"); + SyncState.AddQueryToQueue("zConfiguration Call Layout Size"); + SyncState.AddQueryToQueue("zConfiguration Call Layout Position"); + SyncState.AddQueryToQueue("zConfiguration Call Lock Enable"); + SyncState.AddQueryToQueue("zConfiguration Call MuteUserOnEntry Enable"); + SyncState.AddQueryToQueue("zConfiguration Call ClosedCaption FontSize "); + SyncState.AddQueryToQueue("zConfiguration Call ClosedCaption Visible"); + + // zCommand + + SyncState.AddQueryToQueue("zCommand Phonebook List Offset: 0 Limit: 512"); + SyncState.AddQueryToQueue("zCommand Bookings List"); + + + SyncState.StartSync(); + } + + /// + /// Processes messages as they are dequeued + /// + /// + void ProcessMessage(string message) + { + // Counts the curly braces + if(message.Contains('{')) + JsonCurlyBraceCounter++; + + if (message.Contains('}')) + JsonCurlyBraceCounter--; + + Debug.Console(2, this, "JSON Curly Brace Count: {0}", JsonCurlyBraceCounter); + + if (!JsonFeedbackMessageIsIncoming && message.Trim('\x20') == "{" + Delimiter) // Check for the beginning of a new JSON message + { + JsonFeedbackMessageIsIncoming = true; + JsonCurlyBraceCounter = 1; // reset the counter for each new message + + JsonMessage = new StringBuilder(); + + JsonMessage.Append(message); + + if (CommDebuggingIsOn) + Debug.Console(2, this, "Incoming JSON message..."); + + return; + } + else if (JsonFeedbackMessageIsIncoming && message.Trim('\x20') == "}" + Delimiter) // Check for the end of a JSON message + { + JsonMessage.Append(message); + + if(JsonCurlyBraceCounter == 0) + { + JsonFeedbackMessageIsIncoming = false; + + if (CommDebuggingIsOn) + Debug.Console(2, this, "Complete JSON Received:\n{0}", JsonMessage.ToString()); + + // Forward the complete message to be deserialized + DeserializeResponse(JsonMessage.ToString()); + } + + //JsonMessage = new StringBuilder(); + return; + } + + // NOTE: This must happen after the above conditions have been checked + // Append subsequent partial JSON fragments to the string builder + if (JsonFeedbackMessageIsIncoming) + { + JsonMessage.Append(message); + + //Debug.Console(1, this, "Building JSON:\n{0}", JsonMessage.ToString()); + return; + } + + if (CommDebuggingIsOn) + Debug.Console(1, this, "Non-JSON response: '{0}'", message); + + JsonCurlyBraceCounter = 0; // reset on non-JSON response + + if (!SyncState.InitialSyncComplete) + { + switch (message.Trim().ToLower()) // remove the whitespace + { + case "*r login successful": + { + SyncState.LoginMessageReceived(); + + // Fire up a thread to send the intial commands. + CrestronInvoke.BeginInvoke((o) => + { + Thread.Sleep(100); + // disable echo of commands + SendText("echo off"); + Thread.Sleep(100); + // set feedback exclusions + SendText("zFeedback Register Op: ex Path: /Event/InfoResult/info/callin_country_list"); + Thread.Sleep(100); + SendText("zFeedback Register Op: ex Path: /Event/InfoResult/info/callout_country_list"); + Thread.Sleep(100); + // switch to json format + SendText("format json"); + }); + + break; + } + } + } + } + + /// + /// Deserializes a JSON formatted response + /// + /// + void DeserializeResponse(string response) + { + try + { + var trimmedResponse = response.Trim(); + + if (trimmedResponse.Length <= 0) + return; + + var message = JObject.Parse(trimmedResponse); + + eZoomRoomResponseType eType = (eZoomRoomResponseType)Enum.Parse(typeof(eZoomRoomResponseType), message["type"].Value(), true); + + var topKey = message["topKey"].Value(); + + var responseObj = message[topKey]; + + Debug.Console(1, "{0} Response Received. topKey: '{1}'\n{2}", eType, topKey, responseObj.ToString()); + + switch (eType) + { + case eZoomRoomResponseType.zConfiguration: + { + switch (topKey.ToLower()) + { + case "call": + { + JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Call); + + break; + } + case "audio": + { + JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Audio); + + break; + } + case "video": + { + JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Video); + + break; + } + case "client": + { + JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Client); + + break; + } + default: + { + break; + } + } + break; + } + case eZoomRoomResponseType.zCommand: + { + switch (topKey.ToLower()) + { + case "phonebooklistresult": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.Phonebook); + + if(!PhonebookSyncState.InitialSyncComplete) + { + PhonebookSyncState.InitialPhonebookFoldersReceived(); + PhonebookSyncState.PhonebookRootEntriesReceived(); + PhonebookSyncState.SetPhonebookHasFolders(false); + PhonebookSyncState.SetNumberOfContacts(Status.Phonebook.Contacts.Count); + } + + var directoryResults = new CodecDirectory(); + + directoryResults = zStatus.Phonebook.ConvertZoomContactsToGeneric(Status.Phonebook.Contacts); + + DirectoryRoot = directoryResults; + + OnDirectoryResultReturned(directoryResults); + + break; + } + case "listparticipantsresult": + { + Debug.Console(1, this, "JTokenType: {0}", responseObj.Type); + + if (responseObj.Type == JTokenType.Array) + { + // if the type is array this must be the complete list + Status.Call.Participants = JsonConvert.DeserializeObject>(responseObj.ToString()); + } + else if (responseObj.Type == JTokenType.Object) + { + // this is a single participant event notification + + var participant = JsonConvert.DeserializeObject(responseObj.ToString()); + + if (participant != null) + { + if (participant.Event == "ZRCUserChangedEventLeftMeeting" || participant.Event == "ZRCUserChangedEventUserInfoUpdated") + { + var existingParticipant = Status.Call.Participants.FirstOrDefault(p => p.UserId.Equals(participant.UserId)); + + if (existingParticipant != null) + { + if (participant.Event == "ZRCUserChangedEventLeftMeeting") + { + // Remove participant + Status.Call.Participants.Remove(existingParticipant); + } + else if (participant.Event == "ZRCUserChangedEventUserInfoUpdated") + { + // Update participant + JsonConvert.PopulateObject(responseObj.ToString(), existingParticipant); + } + } + } + else if(participant.Event == "ZRCUserChangedEventJoinedMeeting") + { + Status.Call.Participants.Add(participant); + } + } + } + + PrintCurrentCallParticipants(); + + break; + } + default: + { + break; + } + } + break; + } + case eZoomRoomResponseType.zEvent: + { + switch (topKey.ToLower()) + { + case "phonebook": + { + if (responseObj["Updated Contact"] != null) + { + var updatedContact = JsonConvert.DeserializeObject(responseObj["Updated Contact"].ToString()); + + var existingContact = Status.Phonebook.Contacts.FirstOrDefault(c => c.Jid.Equals(updatedContact.Jid)); + + if (existingContact != null) + { + // Update existing contact + JsonConvert.PopulateObject(responseObj["Updated Contact"].ToString(), existingContact); + } + } + else if (responseObj["Added Contact"] != null) + { + var newContact = JsonConvert.DeserializeObject(responseObj["Updated Contact"].ToString()); + + // Add a new contact + Status.Phonebook.Contacts.Add(newContact); + } + + break; + } + case "bookingslistresult": + { + if (!SyncState.InitialSyncComplete) + SyncState.LastQueryResponseReceived(); + + var codecBookings = new List(); + + codecBookings = JsonConvert.DeserializeObject < List>(responseObj.ToString()); + + if (codecBookings != null && codecBookings.Count > 0) + { + CodecSchedule.Meetings = zCommand.GetGenericMeetingsFromBookingResult(codecBookings); + } + + break; + } + case "bookings": + { + // Bookings have been updated, trigger a query to retreive the new bookings + if (responseObj["Updated"] != null) + GetBookings(); + + break; + } + case "sharingstate": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.Call.Sharing); + + break; + } + case "incomingcallindication": + { + var incomingCall = JsonConvert.DeserializeObject(responseObj.ToString()); + + if (incomingCall != null) + { + var newCall = new CodecActiveCallItem(); + + newCall.Direction = eCodecCallDirection.Incoming; + newCall.Status = eCodecCallStatus.Ringing; + newCall.Type = eCodecCallType.Unknown; + newCall.Name = incomingCall.callerName; + newCall.Id = incomingCall.callerJID; + + ActiveCalls.Add(newCall); + + OnCallStatusChange(newCall); + } + + break; + } + case "treatedincomingcallindication": + { + var incomingCall = JsonConvert.DeserializeObject(responseObj.ToString()); + + if (incomingCall != null) + { + var existingCall = ActiveCalls.FirstOrDefault(c => c.Id.Equals(incomingCall.callerJID)); + + if (existingCall != null) + { + if (!incomingCall.accepted) + { + existingCall.Status = eCodecCallStatus.Disconnected; + } + else + { + existingCall.Status = eCodecCallStatus.Connecting; + } + + OnCallStatusChange(existingCall); + } + + UpdateCallStatus(); + } + + break; + } + case "calldisconnect": + { + var disconnectEvent = JsonConvert.DeserializeObject(responseObj.ToString()); + + if (disconnectEvent.Successful) + { + if (ActiveCalls.Count > 0) + { + var activeCall = ActiveCalls.FirstOrDefault(c => c.IsActiveCall); + + if (activeCall != null) + { + activeCall.Status = eCodecCallStatus.Disconnected; + + OnCallStatusChange(activeCall); + } + } + } + + UpdateCallStatus(); + break; + } + case "callconnecterror": + { + UpdateCallStatus(); + break; + } + case "videounmuterequest": + { + // TODO: notify room of a request to unmute video + break; + } + case "meetingneedspassword": + { + // TODO: notify user to enter a password + break; + } + case "needwaitforhost": + { + var needWait = JsonConvert.DeserializeObject(responseObj.ToString()); + + if (needWait.Wait) + { + // TODO: notify user to wait for host + } + + break; + } + case "openvideofailforhoststop": + { + // TODO: notify user that host has disabled unmuting video + break; + } + case "updatedcallrecordinfo": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.Call.CallRecordInfo); + + break; + } + default: + { + break; + } + } + break; + } + case eZoomRoomResponseType.zStatus: + { + switch (topKey.ToLower()) + { + case "login": + { + SyncState.LoginMessageReceived(); + + if (!SyncState.InitialQueryMessagesWereSent) + SetUpSyncQueries(); + + JsonConvert.PopulateObject(responseObj.ToString(), Status.Login); + + break; + } + case "systemunit": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.SystemUnit); + + break; + } + case "call": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.Call); + + UpdateCallStatus(); + + break; + } + case "capabilities": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.Capabilities); + break; + } + case "sharing": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.Sharing); + + break; + } + case "numberofscreens": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.NumberOfScreens); + break; + } + case "video": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.Video); + break; + } + case "camerashare": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.CameraShare); + break; + } + case "layout": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.Layout); + break; + } + case "audio input line": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.AudioInputs); + break; + } + case "audio output line": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.AudioOuputs); + break; + } + case "video camera line": + { + JsonConvert.PopulateObject(responseObj.ToString(), Status.Cameras); + + if(!SyncState.CamerasHaveBeenSetUp) + SetUpCameras(); + + break; + } + default: + { + break; + } + } + + break; + } + default: + { + Debug.Console(1, "Unknown Response Type:"); + break; + } + } + + } + catch (Exception ex) + { + Debug.Console(1, this, "Error Deserializing feedback: {0}", ex); + } + } + + public void PrintCurrentCallParticipants() + { + if (Debug.Level > 0) + { + Debug.Console(1, this, "****************************Call Participants***************************"); + foreach (var participant in Status.Call.Participants) + { + Debug.Console(1, this, "Name: {0} Audio: {1} IsHost: {2}", participant.UserName, participant.AudioStatusState, participant.IsHost); + } + Debug.Console(1, this, "************************************************************************"); + } + } + + /// + /// Retrieves bookings list + /// + void GetBookings() + { + SendText("zCommand Bookings List"); + } + + + /// + /// Updates the current call status + /// + void UpdateCallStatus() + { + zStatus.eCallStatus callStatus; + + if (Status.Call != null) + { + callStatus = Status.Call.Status; + + // If not currently in a meeting, intialize the call object + if (callStatus != zStatus.eCallStatus.IN_MEETING || callStatus != zStatus.eCallStatus.CONNECTING_MEETING) + { + Status.Call = new zStatus.Call(); + Status.Call.Status = callStatus; // set the status after initializing the object + } + + if (ActiveCalls.Count == 0) + { + if(callStatus == zStatus.eCallStatus.CONNECTING_MEETING) + { + var newCall = new CodecActiveCallItem(); + + newCall.Status = eCodecCallStatus.Connecting; + + ActiveCalls.Add(newCall); + + OnCallStatusChange(newCall); + } + } + else + { + var existingCall = ActiveCalls.FirstOrDefault(c => !c.Status.Equals(eCodecCallStatus.Ringing)); + + if (callStatus == zStatus.eCallStatus.IN_MEETING) + { + existingCall.Status = eCodecCallStatus.Connected; + } + else if (callStatus == zStatus.eCallStatus.NOT_IN_MEETING) + { + existingCall.Status = eCodecCallStatus.Disconnected; + } + + OnCallStatusChange(existingCall); + } + + } + + Debug.Console(1, this, "****************************Active Calls*********************************"); + + // Clean up any disconnected calls left in the list + for (int i = 0; i < ActiveCalls.Count; i++) + { + var call = ActiveCalls[i]; + + Debug.Console(1, this, + @"Name: {0} + ID: {1} + IsActive: {2} + Status: {3} + Direction: {4}", call.Name, call.Id, call.IsActiveCall, call.Status, call.Direction); + + if (!call.IsActiveCall) + { + Debug.Console(1, this, "******Removing Inactive Call: {0}******", call.Name); + ActiveCalls.Remove(call); + } + } + + Debug.Console(1, this, "**************************************************************************"); + + } + + public override void StartSharing() + { + throw new NotImplementedException(); + } + + /// + /// Stops sharing the current presentation + /// + public override void StopSharing() + { + SendText("zCommand Call Sharing Disconnect"); + } + + public override void PrivacyModeOn() + { + SendText("zConfiguration Call Microphone Mute: on"); + } + + public override void PrivacyModeOff() + { + SendText("zConfiguration Call Microphone Mute: off"); + } + + public override void PrivacyModeToggle() + { + if (PrivacyModeIsOnFeedback.BoolValue) + PrivacyModeOff(); + else + PrivacyModeOn(); + } + + public override void MuteOff() + { + SetVolume((ushort)PreviousVolumeLevel); + } + + public override void MuteOn() + { + PreviousVolumeLevel = Configuration.Audio.Output.Volume; // Store the previous level for recall + + SetVolume(0); + } + + public override void MuteToggle() + { + if (MuteFeedback.BoolValue) + MuteOff(); + else + MuteOn(); + } + + /// + /// Increments the voluem + /// + /// + public override void VolumeUp(bool pressRelease) + { + // TODO: Implment volume increment that calls SetVolume() + } + + /// + /// Decrements the volume + /// + /// + public override void VolumeDown(bool pressRelease) + { + // TODO: Implment volume decrement that calls SetVolume() + } + + /// + /// 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("zConfiguration Audio Output volume: {0}", scaledLevel)); + } + + /// + /// Recalls the default volume on the codec + /// + public void VolumeSetToDefault() + { + + } + + /// + /// + /// + public override void StandbyActivate() + { + // No corresponding function on device + } + + /// + /// + /// + public override void StandbyDeactivate() + { + // No corresponding function on device + } + + public override void ExecuteSwitch(object selector) + { + (selector as Action)(); + } + + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) + { + ExecuteSwitch(inputSelector); + } + + public override void AcceptCall(CodecActiveCallItem call) + { + var incomingCall = ActiveCalls.FirstOrDefault(c => c.Status.Equals(eCodecCallStatus.Ringing) && c.Direction.Equals(eCodecCallDirection.Incoming)); + SendText(string.Format("zCommand Call Accept callerJID: {0}", incomingCall.Id)); + } + + public override void RejectCall(CodecActiveCallItem call) + { + var incomingCall = ActiveCalls.FirstOrDefault(c => c.Status.Equals(eCodecCallStatus.Ringing) && c.Direction.Equals(eCodecCallDirection.Incoming)); + SendText(string.Format("zCommand Call Reject callerJID: {0}", incomingCall.Id)); + } + + public override void Dial(Meeting meeting) + { + SendText(string.Format("zCommand Dial Start meetingNumber: {0}", meeting.Id)); + } + + public override void Dial(string number) + { + SendText(string.Format("zCommand Dial Join meetingNumber: {0}", number)); + } + + /// + /// Invites a contact to either a new meeting (if not already in a meeting) or the current meeting. + /// Currently only invites a single user + /// + /// + public override void Dial(IInvitableContact contact) + { + var ic = contact as zStatus.ZoomDirectoryContact; + + if (ic != null) + { + Debug.Console(1, this, "Attempting to Dial (Invite): {0}", ic.Name); + + if (!IsInCall) + SendText(string.Format("zCommand Invite Duration: {0} user: {1}", DefaultMeetingDurationMin, ic.ContactId)); + else + SendText(string.Format("zCommand Call invite user: {0}", ic.ContactId)); + } + } + + public override void EndCall(CodecActiveCallItem call) + { + SendText("zCommand Call Disconnect"); + } + + public override void EndAllCalls() + { + SendText("zCommand Call Disconnect"); + } + + public override void SendDtmf(string s) + { + throw new NotImplementedException(); + } + + + #region IHasCodecSelfView Members + + public BoolFeedback SelfviewIsOnFeedback { get; private set; } + + public void SelfViewModeOn() + { + SendText("zConfiguration Video hide_conf_self_video: off"); + } + + public void SelfViewModeOff() + { + SendText("zConfiguration Video hide_conf_self_video: on"); + } + + public void SelfViewModeToggle() + { + if (SelfviewIsOnFeedback.BoolValue) + SelfViewModeOff(); + else + SelfViewModeOn(); + } + + #endregion + + #region IHasDirectory Members + + public event EventHandler DirectoryResultReturned; + + /// 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); + } + + public CodecDirectory DirectoryRoot { get; private set; } + + public CodecDirectory CurrentDirectoryResult + { + get + { + if (DirectoryBrowseHistory.Count > 0) + return DirectoryBrowseHistory[DirectoryBrowseHistory.Count - 1]; + else + return DirectoryRoot; + } + } + + public CodecPhonebookSyncState PhonebookSyncState { get; private set; } + + public void SearchDirectory(string searchString) + { + var directoryResults = new CodecDirectory(); + + directoryResults.AddContactsToDirectory(DirectoryRoot.CurrentDirectoryResults.FindAll(c => c.Name.IndexOf(searchString, 0, StringComparison.OrdinalIgnoreCase) > -1)); + + DirectoryBrowseHistory.Add(directoryResults); + + OnDirectoryResultReturned(directoryResults); + } + + public void GetDirectoryFolderContents(string folderId) + { + var directoryResults = new CodecDirectory(); + + directoryResults.ResultsFolderId = folderId; + directoryResults.AddContactsToDirectory(DirectoryRoot.CurrentDirectoryResults.FindAll(c => c.FolderId.Equals(folderId))); + + DirectoryBrowseHistory.Add(directoryResults); + + OnDirectoryResultReturned(directoryResults); + } + + public void SetCurrentDirectoryToRoot() + { + DirectoryBrowseHistory.Clear(); + + OnDirectoryResultReturned(DirectoryRoot); + } + + 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); + } + + public BoolFeedback CurrentDirectoryResultIsNotDirectoryRoot { get; private set; } + + public List DirectoryBrowseHistory { get; private set; } + + #endregion + + #region IHasScheduleAwareness Members + + public CodecScheduleAwareness CodecSchedule { get; private set; } + + public void GetSchedule() + { + GetBookings(); + } + + #endregion + + /// + /// Builds the cameras List by using the Zoom Room zStatus.Cameras data. Could later be modified to build from config data + /// + void SetUpCameras() + { + SelectedCameraFeedback = new StringFeedback(() => Configuration.Video.Camera.SelectedId); + + ControllingFarEndCameraFeedback = new BoolFeedback(() => SelectedCamera is IAmFarEndCamera); + + foreach (var cam in Status.Cameras) + { + var camera = new ZoomRoomCamera(cam.id, cam.Name, this); + + Cameras.Add(camera); + + if (cam.Selected) + SelectedCamera = camera; + } + + if (IsInCall) + UpdateFarEndCameras(); + + SyncState.CamerasSetUp(); + } + + /// + /// Dynamically creates far end cameras for call participants who have far end control enabled. + /// + void UpdateFarEndCameras() + { + // TODO: set up far end cameras for the current call + } + + #region IHasCameras Members + + public event EventHandler CameraSelected; + + public List Cameras { get; private set; } + + private CameraBase _selectedCamera; + + public CameraBase SelectedCamera + { + get + { + return _selectedCamera; + } + private set + { + _selectedCamera = value; + SelectedCameraFeedback.FireUpdate(); + ControllingFarEndCameraFeedback.FireUpdate(); + + var handler = CameraSelected; + if (handler != null) + { + handler(this, new CameraSelectedEventArgs(SelectedCamera)); + } + } + } + + + public StringFeedback SelectedCameraFeedback { get; private set; } + + public void SelectCamera(string key) + { + if(Cameras != null) + { + var camera = Cameras.FirstOrDefault(c => c.Key.IndexOf(key, StringComparison.OrdinalIgnoreCase) > -1); + if (camera != null) + { + Debug.Console(1, this, "Selected Camera with key: '{0}'", camera.Key); + SelectedCamera = camera; + } + else + Debug.Console(1, this, "Unable to select camera with key: '{0}'", key); + } + } + + #endregion + + #region IHasFarEndCameraControl Members + + public CameraBase FarEndCamera { get; private set; } + + public BoolFeedback ControllingFarEndCameraFeedback { get; private set; } + + #endregion + } + + /// + /// Zoom Room specific info object + /// + public class ZoomRoomInfo : VideoCodecInfo + { + public ZoomRoomStatus Status { get; private set; } + public ZoomRoomConfiguration Configuration { get; private set; } + + public override bool AutoAnswerEnabled + { + get + { + return Status.SystemUnit.RoomInfo.AutoAnswerIsEnabled; + } + } + + public override string E164Alias + { + get + { + if (!string.IsNullOrEmpty(Status.SystemUnit.MeetingNumber)) + return Status.SystemUnit.MeetingNumber; + else + return string.Empty; + } + } + + public override string H323Id + { + get + { + if (!string.IsNullOrEmpty(Status.Call.Info.meeting_list_item.third_party.h323_address)) + return Status.Call.Info.meeting_list_item.third_party.h323_address; + else + return string.Empty; + } + } + + public override string IpAddress + { + get + { + if (!string.IsNullOrEmpty(Status.SystemUnit.RoomInfo.AccountEmail)) + return Status.SystemUnit.RoomInfo.AccountEmail; + else + return string.Empty; + } + } + + public override bool MultiSiteOptionIsEnabled + { + get { return true; } + } + + public override string SipPhoneNumber + { + get + { + if (!string.IsNullOrEmpty(Status.Call.Info.dialIn)) + return Status.Call.Info.dialIn; + else + return string.Empty; + } + } + + public override string SipUri + { + get + { + if (!string.IsNullOrEmpty(Status.Call.Info.meeting_list_item.third_party.sip_address)) + return Status.Call.Info.meeting_list_item.third_party.sip_address; + else + return string.Empty; + } + } + + public ZoomRoomInfo(ZoomRoomStatus status, ZoomRoomConfiguration configuration) + { + Status = status; + Configuration = configuration; + } + } + + /// + /// Tracks the initial sycnronization state when establishing a new connection + /// + public class ZoomRoomSyncState : IKeyed + { + bool _InitialSyncComplete; + + public event EventHandler InitialSyncCompleted; + + private CrestronQueue SyncQueries; + + private ZoomRoom Parent; + + 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 InitialQueryMessagesWereSent { get; private set; } + + public bool LastQueryResponseWasReceived { get; private set; } + + public bool CamerasHaveBeenSetUp { get; private set;} + + public ZoomRoomSyncState(string key, ZoomRoom parent) + { + Parent = parent; + Key = key; + SyncQueries = new CrestronQueue(50); + CodecDisconnected(); + } + + public void StartSync() + { + DequeueQueries(); + } + + void DequeueQueries() + { + while (!SyncQueries.IsEmpty) + { + var query = SyncQueries.Dequeue(); + + Parent.SendText(query); + } + + InitialQueryMessagesSent(); + } + + public void AddQueryToQueue(string query) + { + SyncQueries.Enqueue(query); + } + + public void LoginMessageReceived() + { + LoginMessageWasReceived = true; + Debug.Console(1, this, "Login Message Received."); + CheckSyncStatus(); + } + + public void InitialQueryMessagesSent() + { + InitialQueryMessagesWereSent = true; + Debug.Console(1, this, "Query Messages Sent."); + CheckSyncStatus(); + } + + public void LastQueryResponseReceived() + { + LastQueryResponseWasReceived = true; + Debug.Console(1, this, "Last Query Response Received."); + CheckSyncStatus(); + } + + public void CamerasSetUp() + { + CamerasHaveBeenSetUp = true; + Debug.Console(1, this, "Cameras Set Up."); + CheckSyncStatus(); + } + + public void CodecDisconnected() + { + SyncQueries.Clear(); + LoginMessageWasReceived = false; + InitialQueryMessagesWereSent = false; + LastQueryResponseWasReceived = false; + CamerasHaveBeenSetUp = false; + InitialSyncComplete = false; + } + + void CheckSyncStatus() + { + if (LoginMessageWasReceived && InitialQueryMessagesWereSent && LastQueryResponseWasReceived && CamerasHaveBeenSetUp) + { + InitialSyncComplete = true; + Debug.Console(1, this, "Initial Codec Sync Complete!"); + } + else + InitialSyncComplete = false; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomCamera.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomCamera.cs new file mode 100644 index 00000000..32a8296e --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomCamera.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Essentials.Devices.Common.Cameras; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom +{ + public enum eZoomRoomCameraState + { + Start, + Continue, + Stop, + RequestRemote, + GiveupRemote, + RequestedByFarEnd + } + + public enum eZoomRoomCameraAction + { + Left, + Right, + Up, + Down, + In, + Out + } + + + public class ZoomRoomCamera : CameraBase, IHasCameraPtzControl + { + protected ZoomRoom ParentCodec { get; private set; } + + public int Id = 0; // ID of near end selected camara is always 0 + + private int ContinueTime = 10; // number of milliseconds between issuing continue commands + + private CTimer ContinueTimer; + + eZoomRoomCameraAction LastAction; + + private bool isPanning; + + private bool isTilting; + + private bool isZooming; + + private bool isFocusing; + + private bool isMoving + { + get + { + return isPanning || isTilting || isZooming || isFocusing; + + } + } + + public ZoomRoomCamera(string key, string name, ZoomRoom codec) + : base(key, name) + { + ParentCodec = codec; + } + + /// + /// Builds the command and triggers the parent ZoomRoom to send it + /// + /// + /// + /// + void SendCommand(eZoomRoomCameraState state, eZoomRoomCameraAction action) + { + LastAction = action; + ParentCodec.SendText(string.Format("zCommand Call CameraControl Id: {0} State: {1} Action: {2}", Id, state, action)); + } + + void StartContinueTimer() + { + if(ContinueTimer == null) + ContinueTimer = new CTimer((o) => SendContinueAction(LastAction), ContinueTime); + } + + void SendContinueAction(eZoomRoomCameraAction action) + { + SendCommand(eZoomRoomCameraState.Continue, action); + ContinueTimer.Reset(); + } + + void StopContinueTimer() + { + if (ContinueTimer != null) + { + ContinueTimer.Stop(); + ContinueTimer.Dispose(); + } + } + + #region IHasCameraPtzControl Members + + public void PositionHome() + { + throw new NotImplementedException(); + } + + #endregion + + #region IHasCameraPanControl Members + + public void PanLeft() + { + if (!isMoving) + { + SendCommand(eZoomRoomCameraState.Start, eZoomRoomCameraAction.Left); + StartContinueTimer(); + isPanning = true; + } + } + + public void PanRight() + { + if (!isMoving) + { + SendCommand(eZoomRoomCameraState.Start, eZoomRoomCameraAction.Right); + StartContinueTimer(); + isPanning = true; + } + } + + public void PanStop() + { + StopContinueTimer(); + SendCommand(eZoomRoomCameraState.Stop, LastAction); + isPanning = false; + } + + #endregion + + #region IHasCameraTiltControl Members + + public void TiltDown() + { + if (!isMoving) + { + SendCommand(eZoomRoomCameraState.Start, eZoomRoomCameraAction.Down); + StartContinueTimer(); + isTilting = true; + } + } + + public void TiltUp() + { + if (!isMoving) + { + SendCommand(eZoomRoomCameraState.Start, eZoomRoomCameraAction.Up); + StartContinueTimer(); + isTilting = true; + } + } + + public void TiltStop() + { + StopContinueTimer(); + SendCommand(eZoomRoomCameraState.Stop, LastAction); + isTilting = false; + } + + #endregion + + #region IHasCameraZoomControl Members + + public void ZoomIn() + { + if (!isMoving) + { + SendCommand(eZoomRoomCameraState.Start, eZoomRoomCameraAction.In); + StartContinueTimer(); + isZooming = true; + } + } + + public void ZoomOut() + { + if (!isMoving) + { + SendCommand(eZoomRoomCameraState.Start, eZoomRoomCameraAction.Out); + StartContinueTimer(); + isZooming = true; + } + } + + public void ZoomStop() + { + StopContinueTimer(); + SendCommand(eZoomRoomCameraState.Stop, LastAction); + isZooming = false; + } + + #endregion + } + + public class ZoomRoomFarEndCamera : ZoomRoomCamera, IAmFarEndCamera + { + + public ZoomRoomFarEndCamera(string key, string name, ZoomRoom codec, int id) + : base(key, name, codec) + { + Id = id; + } + + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomPropertiesConfig.cs new file mode 100644 index 00000000..d60b63c2 --- /dev/null +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomPropertiesConfig.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom +{ + public class ZoomRoomPropertiesConfig + { + public CommunicationMonitorConfig CommunicationMonitorProperties { get; set; } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Framework.sln b/essentials-framework/Essentials Framework.sln new file mode 100644 index 00000000..c508ad29 --- /dev/null +++ b/essentials-framework/Essentials Framework.sln @@ -0,0 +1,32 @@ + +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash_Essentials_Core", ".\Essentials Core\PepperDashEssentialsBase\PepperDash_Essentials_Core.csproj", "{A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials_DM", ".\Essentials DM\Essentials_DM\Essentials_DM.csproj", "{9199CE8A-0C9F-4952-8672-3EED798B284F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials Devices Common", ".\Essentials Devices Common\Essentials Devices Common\Essentials Devices Common.csproj", "{892B761C-E479-44CE-BD74-243E9214AF13}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5}.Release|Any CPU.Build.0 = Release|Any CPU + {9199CE8A-0C9F-4952-8672-3EED798B284F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9199CE8A-0C9F-4952-8672-3EED798B284F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9199CE8A-0C9F-4952-8672-3EED798B284F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9199CE8A-0C9F-4952-8672-3EED798B284F}.Release|Any CPU.Build.0 = Release|Any CPU + {892B761C-E479-44CE-BD74-243E9214AF13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {892B761C-E479-44CE-BD74-243E9214AF13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {892B761C-E479-44CE-BD74-243E9214AF13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {892B761C-E479-44CE-BD74-243E9214AF13}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/essentials-framework/Essentials/PepperDashEssentials/Bridges/Bridges.BridgeFactory.cs b/essentials-framework/Essentials/PepperDashEssentials/Bridges/Bridges.BridgeFactory.cs new file mode 100644 index 00000000..f65d4e65 --- /dev/null +++ b/essentials-framework/Essentials/PepperDashEssentials/Bridges/Bridges.BridgeFactory.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Core; +using PepperDash.Essentials.Core.Routing; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.EthernetCommunication; + +namespace PepperDash.Essentials { + public class BridgeFactory { + public static IKeyed GetDevice(PepperDash.Essentials.Core.Config.DeviceConfig dc) { + // ? why is this static JTA 2018-06-13? + + var key = dc.Key; + var name = dc.Name; + var type = dc.Type; + var properties = dc.Properties; + var propAnon = new { }; + JsonConvert.DeserializeAnonymousType(dc.Properties.ToString(), propAnon); + + var typeName = dc.Type.ToLower(); + var groupName = dc.Group.ToLower(); + + Debug.Console(0, "Name {0}, Key {1}, Type {2}, Properties {3}", name, key, type, properties.ToString()); + if (typeName == "essentialdm") { + return new EssentialDM(key, name, properties); + } else if (typeName == "essentialcomm") { + + Debug.Console(0, "Launch Essential Comm"); + return new EssentialComm(key, name, properties); + + } + return null; + } + } + public class BridgeApiEisc { + public uint Ipid; + public ThreeSeriesTcpIpEthernetIntersystemCommunications Eisc; + public BridgeApiEisc(string ipid) { + Ipid = (UInt32)int.Parse(ipid, System.Globalization.NumberStyles.HexNumber); + Eisc = new ThreeSeriesTcpIpEthernetIntersystemCommunications(Ipid, "127.0.0.2", Global.ControlSystem); + Eisc.Register(); + Eisc.SigChange += Eisc_SigChange; + Debug.Console(0, "BridgeApiEisc Created at Ipid {0}", ipid); + } + void Eisc_SigChange(object currentDevice, Crestron.SimplSharpPro.SigEventArgs args) { + if (Debug.Level >= 1) + Debug.Console(1, "DDVC EISC change: {0} {1}={2}", args.Sig.Type, args.Sig.Number, args.Sig.StringValue); + var uo = args.Sig.UserObject; + if (uo is Action) + (uo as Action)(args.Sig.BoolValue); + else if (uo is Action) + (uo as Action)(args.Sig.UShortValue); + else if (uo is Action) + (uo as Action)(args.Sig.StringValue); + } + } + + } + + + + \ No newline at end of file diff --git a/essentials-framework/Essentials/PepperDashEssentials/Bridges/EssentialComms.cs b/essentials-framework/Essentials/PepperDashEssentials/Bridges/EssentialComms.cs new file mode 100644 index 00000000..9102a1e2 --- /dev/null +++ b/essentials-framework/Essentials/PepperDashEssentials/Bridges/EssentialComms.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Core; +using PepperDash.Essentials.Core.Routing; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.EthernetCommunication; +using Crestron.SimplSharpPro.CrestronThread; + +namespace PepperDash.Essentials { + public class EssentialCommConfig { + public string[] EiscApiIpids; + public EssentialCommCommConnectionConfigs[] CommConnections; + } + public class EssentialCommCommConnectionConfigs { + public uint joinNumber {get; set; } + public EssentialsControlPropertiesConfig control { get; set; } + } + + public class EssentialCommsPort { + public IBasicCommunication Comm; + public IntFeedback StatusFeedback; + public BoolFeedback ConnectedFeedback; + public List Outputs = new List(); + public String RxBuffer; + public EssentialCommsPort(EssentialsControlPropertiesConfig config, string keyPrefix) { + Comm = CommFactory.CreateCommForConfig(config, keyPrefix); + // var PortGather = new CommunicationGather(Comm, config.EndOfLineChar); + Comm.TextReceived += new EventHandler(Communication_TextReceived); + + var socket = Comm as ISocketStatus; + StatusFeedback = new IntFeedback(() => { return (int)socket.ClientStatus; }); + ConnectedFeedback = new BoolFeedback(() => { return Comm.IsConnected; }); + + if (socket != null) { + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } else { + } + + } + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) { + StatusFeedback.FireUpdate(); + ConnectedFeedback.FireUpdate(); + if (e.Client.IsConnected) { + // Tasks on connect + } else { + // Cleanup items from this session + } + } + void Communication_TextReceived(object sender, GenericCommMethodReceiveTextArgs args) { + try { + foreach (var Output in Outputs) { + Output.Api.Eisc.StringInput[Output.Join].StringValue = args.Text; + } + + } + catch (Exception) { + throw new FormatException(string.Format("ERROR:{0}")); + } + } + } + + public class EssentialComm : Device { + public EssentialCommConfig Properties; + public String Key; + public CommunicationGather PortGather { get; private set; } + public List Apis {get; set;} + public Dictionary CommFeedbacks {get; private set; } + public StatusMonitorBase CommunicationMonitor { get; private set; } + public Dictionary CommDictionary { get; private set; } + + public EssentialComm(string key, string name, JToken properties) : base(key, name) { + Properties = JsonConvert.DeserializeObject(properties.ToString()); + CommFeedbacks = new Dictionary(); + CommDictionary = new Dictionary(); + Apis = new List(); + Key = key; + + } + + + + public override bool CustomActivate() + { + int commNumber = 1; + foreach (var commConfig in Properties.CommConnections) { + var commPort = new EssentialCommsPort(commConfig.control, string.Format("{0}-{1}", Key, commConfig.joinNumber)); + CommDictionary.Add(commConfig.joinNumber, commPort); + commNumber++; + } + + foreach (var Ipid in Properties.EiscApiIpids) { + var ApiEisc = new BridgeApiEisc(Ipid); + Apis.Add(ApiEisc); + foreach (var commConnection in CommDictionary) { + Debug.Console(0, "Joining Api{0} to comm {1}", Ipid, commConnection.Key); + var tempComm = commConnection.Value; + var tempJoin = (uint)commConnection.Key; + EssentialComApiMap ApiMap = new EssentialComApiMap(ApiEisc, (uint)tempJoin); + + tempComm.Outputs.Add(ApiMap); + // Check for ApiMap Overide Values here + + ApiEisc.Eisc.SetBoolSigAction(tempJoin, b => { + if (b) { tempComm.Comm.Connect(); } else { tempComm.Comm.Disconnect(); } + }); + ApiEisc.Eisc.SetStringSigAction(tempJoin, s => tempComm.Comm.SendText(s)); + + tempComm.StatusFeedback.LinkInputSig(ApiEisc.Eisc.UShortInput[tempJoin]); + tempComm.ConnectedFeedback.LinkInputSig(ApiEisc.Eisc.BooleanInput[tempJoin]); + + + + } + + } + + return true; + } + + + } + public class EssentialComApiMap { + public uint Join; + public BridgeApiEisc Api; + public uint connectJoin; + public EssentialComApiMap(BridgeApiEisc api, uint join) { + Join = join; + Api = api; + } + } + } \ No newline at end of file diff --git a/essentials-framework/Essentials/PepperDashEssentials/Bridges/EssentialDM.cs b/essentials-framework/Essentials/PepperDashEssentials/Bridges/EssentialDM.cs new file mode 100644 index 00000000..f6484ca1 --- /dev/null +++ b/essentials-framework/Essentials/PepperDashEssentials/Bridges/EssentialDM.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Core; +using PepperDash.Essentials.Core.Routing; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.EthernetCommunication; + +namespace PepperDash.Essentials { + public class EssentialDM : PepperDash.Core.Device { + public EssentialDMProperties Properties; + public List BridgeApiEiscs; + private PepperDash.Essentials.DM.DmChassisController DmSwitch; + private EssentialDMApiMap ApiMap = new EssentialDMApiMap(); + public EssentialDM(string key, string name, JToken properties) + : base(key, name) { + Properties = JsonConvert.DeserializeObject(properties.ToString()); + + + } + public override bool CustomActivate() { + // Create EiscApis + try { + foreach (var device in DeviceManager.AllDevices) { + if (device.Key == this.Properties.connectionDeviceKey) { + Debug.Console(0, "deviceKey {0} Matches", device.Key); + DmSwitch = DeviceManager.GetDeviceForKey(device.Key) as PepperDash.Essentials.DM.DmChassisController; + + } else { + Debug.Console(0, "deviceKey {0} doesn't match", device.Key); + } + } + if (Properties.EiscApiIpids != null) { + foreach (string Ipid in Properties.EiscApiIpids) { + var ApiEisc = new BridgeApiEisc(Ipid); + // BridgeApiEiscs.Add(ApiEisc); + //Essentials.Core.CommFactory.CreateCommForConfig(); + + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputVideoRoutes[1], u => DmSwitch.ExecuteSwitch(u, 1, eRoutingSignalType.Video)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputVideoRoutes[2], u => DmSwitch.ExecuteSwitch(u, 2, eRoutingSignalType.Video)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputVideoRoutes[3], u => DmSwitch.ExecuteSwitch(u, 3, eRoutingSignalType.Video)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputVideoRoutes[4], u => DmSwitch.ExecuteSwitch(u, 4, eRoutingSignalType.Video)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputVideoRoutes[5], u => DmSwitch.ExecuteSwitch(u, 5, eRoutingSignalType.Video)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputVideoRoutes[6], u => DmSwitch.ExecuteSwitch(u, 6, eRoutingSignalType.Video)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputVideoRoutes[7], u => DmSwitch.ExecuteSwitch(u, 7, eRoutingSignalType.Video)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputVideoRoutes[8], u => DmSwitch.ExecuteSwitch(u, 8, eRoutingSignalType.Video)); + + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputAudioRoutes[1], u => DmSwitch.ExecuteSwitch(u, 1, eRoutingSignalType.Audio)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputAudioRoutes[2], u => DmSwitch.ExecuteSwitch(u, 2, eRoutingSignalType.Audio)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputAudioRoutes[3], u => DmSwitch.ExecuteSwitch(u, 3, eRoutingSignalType.Audio)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputAudioRoutes[4], u => DmSwitch.ExecuteSwitch(u, 4, eRoutingSignalType.Audio)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputAudioRoutes[5], u => DmSwitch.ExecuteSwitch(u, 5, eRoutingSignalType.Audio)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputAudioRoutes[6], u => DmSwitch.ExecuteSwitch(u, 6, eRoutingSignalType.Audio)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputAudioRoutes[7], u => DmSwitch.ExecuteSwitch(u, 7, eRoutingSignalType.Audio)); + ApiEisc.Eisc.SetUShortSigAction(ApiMap.OutputAudioRoutes[8], u => DmSwitch.ExecuteSwitch(u, 8, eRoutingSignalType.Audio)); + + foreach (var output in DmSwitch.Chassis.Outputs) { + Debug.Console(0, "Creating EiscActions {0}", output.Number); + + DmSwitch.InputEndpointOnlineFeedbacks[(ushort)output.Number].LinkInputSig(ApiEisc.Eisc.BooleanInput[(ushort)(output.Number + 300)]); + + + /* This wont work...routes to 8 every time i tried for loops, forweach. For some reason the test number keeps going to max value of the loop + * always routing testNum to MaxLoopValue, the above works though.*/ + + for (uint testNum = 1; testNum < 8; testNum++) { + uint num = testNum; + ApiEisc.Eisc.SetUShortSigAction((ushort)(output.Number + 300), u => DmSwitch.ExecuteSwitch(u, num, eRoutingSignalType.Audio)); + ApiEisc.Eisc.SetUShortSigAction((ushort)(output.Number + 100), u => DmSwitch.ExecuteSwitch(u, num, eRoutingSignalType.Video)); + } + + DmSwitch.OutputRouteFeedbacks[(ushort)output.Number].LinkInputSig(ApiEisc.Eisc.UShortInput[ApiMap.OutputVideoRoutes[(int)output.Number]]); + } + DmSwitch.IsOnline.LinkInputSig(ApiEisc.Eisc.BooleanInput[ApiMap.ChassisOnline]); + ApiEisc.Eisc.Register(); + } + } + + + + Debug.Console(0, "Name {0} Activated", this.Name); + return true; + } catch (Exception e) { + Debug.Console(0, "BRidge {0}", e); + return false; + } + } + } + public class EssentialDMProperties { + public string connectionDeviceKey; + public string[] EiscApiIpids; + + + } + + + public class EssentialDMApiMap { + public ushort ChassisOnline = 11; + public Dictionary OutputVideoRoutes; + public Dictionary OutputAudioRoutes; + + public EssentialDMApiMap() { + OutputVideoRoutes = new Dictionary(); + OutputAudioRoutes = new Dictionary(); + + for (int x = 1; x <= 200; x++) { + // Debug.Console(0, "Init Value {0}", x); + OutputVideoRoutes[x] = (ushort)(x + 100); + OutputAudioRoutes[x] = (ushort)(x + 300); + } + } + } + } + \ No newline at end of file diff --git a/essentials-framework/Essentials/PepperDashEssentials/Configuration Original/Factories/DmFactory.cs b/essentials-framework/Essentials/PepperDashEssentials/Configuration Original/Factories/DmFactory.cs new file mode 100644 index 00000000..0a0d4370 --- /dev/null +++ b/essentials-framework/Essentials/PepperDashEssentials/Configuration Original/Factories/DmFactory.cs @@ -0,0 +1,96 @@ +using System; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; +using Crestron.SimplSharpPro.DM.Endpoints.Receivers; +using Crestron.SimplSharpPro.DM.Endpoints.Transmitters; + +using Newtonsoft.Json.Linq; + +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices; +//using PepperDash.Essentials.Devices.Dm; +using PepperDash.Core; + +namespace PepperDash.Essentials +{ + public class DmFactory + { + public static Device Create(JToken devToken) + { + Device dev = null; + try + { + var devType = devToken.Value("type"); + var devKey = devToken.Value("key"); + var devName = devToken.Value("name"); + // Catch all 200 series TX + var devprops = devToken["properties"]; + var ipId = Convert.ToUInt32(devprops.Value("ipId"), 16); + var parent = devprops.Value("parent"); + if (parent == null) + parent = "controlSystem"; + + if (devType.StartsWith("DmTx2", StringComparison.OrdinalIgnoreCase)) + { + DmTx201C tx; + if (parent.Equals("controlSystem", StringComparison.OrdinalIgnoreCase)) + { + tx = new DmTx201C(ipId, Global.ControlSystem); + //dev = new DmTx201SBasicController(devKey, devName, tx); + } + + } + else if (devType.StartsWith("DmRmc", StringComparison.OrdinalIgnoreCase)) + { + DmRmc100C rmc; + if (parent.Equals("controlSystem", StringComparison.OrdinalIgnoreCase)) + { + rmc = new DmRmc100C(ipId, Global.ControlSystem); + //dev = new DmRmcBaseController(devKey, devName, rmc); + } + } + else + FactoryHelper.HandleUnknownType(devToken, devType); + } + catch (Exception e) + { + FactoryHelper.HandleDeviceCreationError(devToken, e); + } + return dev; + } + + + public static Device CreateChassis(JToken devToken) + { + Device dev = null; + try + { + var devType = devToken.Value("type"); + var devKey = devToken.Value("key"); + var devName = devToken.Value("name"); + // Catch all 200 series TX + var devprops = devToken["properties"]; + var ipId = Convert.ToUInt32(devprops.Value("ipId"), 16); + var parent = devprops.Value("parent"); + if (parent == null) + parent = "controlSystem"; + + if (devType.Equals("dmmd8x8", StringComparison.OrdinalIgnoreCase)) + { + var dm = new DmMd8x8(ipId, Global.ControlSystem); + //dev = new DmChassisController(devKey, devName, dm); + } + else if (devType.Equals("dmmd16x16", StringComparison.OrdinalIgnoreCase)) + { + var dm = new DmMd16x16(ipId, Global.ControlSystem); + //dev = new DmChassisController(devKey, devName, dm); + } + } + catch (Exception e) + { + FactoryHelper.HandleDeviceCreationError(devToken, e); + } + return dev; + } + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials/PepperDashEssentials/ControlSystem.cs b/essentials-framework/Essentials/PepperDashEssentials/ControlSystem.cs new file mode 100644 index 00000000..bde618c0 --- /dev/null +++ b/essentials-framework/Essentials/PepperDashEssentials/ControlSystem.cs @@ -0,0 +1,309 @@ +using System; +using System.Linq; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.CrestronThread; +using PepperDash.Core; +// using PepperDash.Core.PortalSync; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common; +using PepperDash.Essentials.DM; +using PepperDash.Essentials.Fusion; +using PepperDash.Essentials.Room.Cotija; + +namespace PepperDash.Essentials +{ + public class ControlSystem : CrestronControlSystem + { + // PepperDashPortalSyncClient PortalSync; + HttpLogoServer LogoServer; + + public ControlSystem() + : base() + { + Thread.MaxNumberOfUserThreads = 400; + Global.ControlSystem = this; + DeviceManager.Initialize(this); + } + + /// + /// Git 'er goin' + /// + public override void InitializeSystem() + { + CrestronConsole.AddNewConsoleCommand(s => + { + foreach (var tl in TieLineCollection.Default) + CrestronConsole.ConsoleCommandResponse(" {0}\r", tl); + }, + "listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(s => + { + CrestronConsole.ConsoleCommandResponse + ("Current running configuration. This is the merged system and template configuration"); + CrestronConsole.ConsoleCommandResponse(Newtonsoft.Json.JsonConvert.SerializeObject + (ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented)); + }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(s => + { + CrestronConsole.ConsoleCommandResponse("This system can be found at the following URLs:\r" + + "System URL: {0}\r" + + "Template URL: {1}", ConfigReader.ConfigObject.SystemUrl, ConfigReader.ConfigObject.TemplateUrl); + }, "portalinfo", "Shows portal URLS from configuration", ConsoleAccessLevelEnum.AccessOperator); + + GoWithLoad(); + } + + /// + /// Do it, yo + /// + public void GoWithLoad() + { + try + { + CrestronConsole.AddNewConsoleCommand(EnablePortalSync, "portalsync", "Loads Portal Sync", + ConsoleAccessLevelEnum.AccessOperator); + + //PortalSync = new PepperDashPortalSyncClient(); + + Debug.Console(0, "Starting Essentials load from configuration"); + + var filesReady = SetupFilesystem(); + if (filesReady) + { + Debug.Console(0, "Folder structure verified. Loading config..."); + if (!ConfigReader.LoadConfig2()) + return; + + Load(); + Debug.Console(0, "Essentials load complete\r" + + "-------------------------------------------------------------"); + } + else + { + Debug.Console(0, + "------------------------------------------------\r" + + "------------------------------------------------\r" + + "------------------------------------------------\r" + + "Essentials file structure setup completed.\r" + + "Please load config, sgd and ir files and\r" + + "restart program.\r" + + "------------------------------------------------\r" + + "------------------------------------------------\r" + + "------------------------------------------------"); + } + } + catch (Exception e) + { + Debug.Console(0, "FATAL INITIALIZE ERROR. System is in an inconsistent state:\r{0}", e); + } + + } + + /// + /// Verifies filesystem is set up. IR, SGD, and program1 folders + /// + bool SetupFilesystem() + { + Debug.Console(0, "Verifying and/or creating folder structure"); + var appNum = InitialParametersClass.ApplicationNumber; + var configDir = @"\NVRAM\Program" + appNum; + var configExists = Directory.Exists(configDir); + if (!configExists) + Directory.Create(configDir); + + var irDir = string.Format(@"\NVRAM\Program{0}\ir", appNum); + if (!Directory.Exists(irDir)) + Directory.Create(irDir); + + var sgdDir = string.Format(@"\NVRAM\Program{0}\sgd", appNum); + if (!Directory.Exists(sgdDir)) + Directory.Create(sgdDir); + + return configExists; + } + + public void EnablePortalSync(string s) + { + if (s.ToLower() == "enable") + { + CrestronConsole.ConsoleCommandResponse("Portal Sync features enabled"); + // PortalSync = new PepperDashPortalSyncClient(); + } + } + + public void TearDown() + { + Debug.Console(0, "Tearing down existing system"); + DeviceManager.DeactivateAll(); + + TieLineCollection.Default.Clear(); + + foreach (var key in DeviceManager.GetDevices()) + DeviceManager.RemoveDevice(key); + + Debug.Console(0, "Tear down COMPLETE"); + } + + /// + /// + /// + void Load() + { + LoadDevices(); + LoadTieLines(); + LoadRooms(); + LoadLogoServer(); + + DeviceManager.ActivateAll(); + } + + + /// + /// Reads all devices from config and adds them to DeviceManager + /// + public void LoadDevices() + { + foreach (var devConf in ConfigReader.ConfigObject.Devices) + { + + try + { + Debug.Console(0, "Creating device '{0}'", devConf.Key); + // Skip this to prevent unnecessary warnings + if (devConf.Key == "processor") + continue; + + // Try local factory first + var newDev = DeviceFactory.GetDevice(devConf); + + // Then associated library factories + if (newDev == null) + newDev = PepperDash.Essentials.Devices.Common.DeviceFactory.GetDevice(devConf); + if (newDev == null) + newDev = PepperDash.Essentials.DM.DeviceFactory.GetDevice(devConf); + if (newDev == null) + newDev = PepperDash.Essentials.Devices.Displays.DisplayDeviceFactory.GetDevice(devConf); + if (newDev == null) + newDev = PepperDash.Essentials.BridgeFactory.GetDevice(devConf); + + if (newDev != null) + DeviceManager.AddDevice(newDev); + else + Debug.Console(0, "ERROR: Cannot load unknown device type '{0}', key '{1}'.", devConf.Type, devConf.Key); + } + catch (Exception e) + { + Debug.Console(0, "ERROR: Creating device {0}. Skipping device. \r{1}", devConf.Key, e); + } + } + } + + /// + /// Helper method to load tie lines. This should run after devices have loaded + /// + public void LoadTieLines() + { + // In the future, we can't necessarily just clear here because devices + // might be making their own internal sources/tie lines + + var tlc = TieLineCollection.Default; + //tlc.Clear(); + if (ConfigReader.ConfigObject.TieLines == null) + { + return; + } + + foreach (var tieLineConfig in ConfigReader.ConfigObject.TieLines) + { + var newTL = tieLineConfig.GetTieLine(); + if (newTL != null) + tlc.Add(newTL); + } + } + + /// + /// Reads all rooms from config and adds them to DeviceManager + /// + public void LoadRooms() + { + if (ConfigReader.ConfigObject.Rooms == null) + { + Debug.Console(0, "WARNING: Configuration contains no rooms"); + return; + } + + foreach (var roomConfig in ConfigReader.ConfigObject.Rooms) + { + var room = roomConfig.GetRoomObject(); + if (room != null) + { + if (room is EssentialsHuddleSpaceRoom) + { + DeviceManager.AddDevice(room); + + Debug.Console(1, "Room is EssentialsHuddleSpaceRoom, attempting to add to DeviceManager with Fusion"); + DeviceManager.AddDevice(new EssentialsHuddleSpaceFusionSystemControllerBase((EssentialsHuddleSpaceRoom)room, 0xf1)); + + // Cotija bridge + var bridge = new CotijaEssentialsHuddleSpaceRoomBridge(room as EssentialsHuddleSpaceRoom); + AddBridgePostActivationHelper(bridge); // Lets things happen later when all devices are present + DeviceManager.AddDevice(bridge); + } + else if (room is EssentialsHuddleVtc1Room) + { + DeviceManager.AddDevice(room); + + Debug.Console(1, "Room is EssentialsHuddleVtc1Room, attempting to add to DeviceManager with Fusion"); + DeviceManager.AddDevice(new EssentialsHuddleVtc1FusionController((EssentialsHuddleVtc1Room)room, 0xf1)); + } + else + { + Debug.Console(1, "Room is NOT EssentialsHuddleSpaceRoom, attempting to add to DeviceManager w/o Fusion"); + DeviceManager.AddDevice(room); + } + + } + else + Debug.Console(0, "WARNING: Cannot create room from config, key '{0}'", roomConfig.Key); + } + } + + /// + /// Helps add the post activation steps that link bridges to main controller + /// + /// + void AddBridgePostActivationHelper(CotijaBridgeBase bridge) + { + bridge.AddPostActivationAction(() => + { + var parent = DeviceManager.AllDevices.FirstOrDefault(d => d.Key == "appServer") as CotijaSystemController; + if (parent == null) + { + Debug.Console(0, bridge, "ERROR: Cannot connect bridge. System controller not present"); + } + Debug.Console(0, bridge, "Linking to parent controller"); + bridge.AddParent(parent); + parent.AddBridge(bridge); + }); + } + + /// + /// Fires up a logo server if not already running + /// + void LoadLogoServer() + { + try + { + LogoServer = new HttpLogoServer(8080, @"\html\logo"); + } + catch (Exception) + { + Debug.Console(0, "NOTICE: Logo server cannot be started. Likely already running in another program"); + } + } + } +} diff --git a/essentials-framework/Essentials/PepperDashEssentials/PepperDashEssentials.csproj b/essentials-framework/Essentials/PepperDashEssentials/PepperDashEssentials.csproj new file mode 100644 index 00000000..463928a4 --- /dev/null +++ b/essentials-framework/Essentials/PepperDashEssentials/PepperDashEssentials.csproj @@ -0,0 +1,224 @@ + + + Release + AnyCPU + 9.0.30729 + 2.0 + {1BED5BA9-88C4-4365-9362-6F4B128071D3} + Library + Properties + PepperDash.Essentials + PepperDashEssentials + {0B4745B0-194B-4BB6-8E21-E9057CA92230};{4D628B5B-2FBC-4AA6-8C16-197242AEB884};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + WindowsCE + E2BECB1F-8C8C-41ba-B736-9BE7D946A398 + 5.0 + SmartDeviceProject1 + v3.5 + Windows CE + + + + + .allowedReferenceRelatedFileExtensions + true + full + false + bin\ + DEBUG;TRACE; + prompt + 4 + 512 + true + true + off + + + .allowedReferenceRelatedFileExtensions + none + true + bin\ + prompt + 4 + 512 + true + true + off + + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.DeviceSupport.dll + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.DM.dll + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.EthernetCommunications.dll + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.Fusion.dll + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.Remotes.dll + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.UI.dll + + + + False + ..\..\Release Package\PepperDash_Core.dll + + + False + ..\..\Essentials DM\Essentials_DM\bin\PepperDash_Essentials_DM.dll + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpCustomAttributesInterface.dll + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpHelperInterface.dll + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpNewtonsoft.dll + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpPro.exe + + + False + ..\..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpReflectionInterface.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {A49AD6C8-FC0A-4CC0-9089-DFB4CF92D2B5} + PepperDash_Essentials_Core + + + {892B761C-E479-44CE-BD74-243E9214AF13} + Essentials Devices Common + + + + + + + + + rem S# Pro preparation will execute after these operations + + \ No newline at end of file diff --git a/essentials-framework/Essentials/PepperDashEssentials/UIDrivers/DualDisplayRouting REMOVE.cs b/essentials-framework/Essentials/PepperDashEssentials/UIDrivers/DualDisplayRouting REMOVE.cs new file mode 100644 index 00000000..1aeb7a3c --- /dev/null +++ b/essentials-framework/Essentials/PepperDashEssentials/UIDrivers/DualDisplayRouting REMOVE.cs @@ -0,0 +1,231 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Crestron.SimplSharp; +//using Crestron.SimplSharpPro; +//using Crestron.SimplSharpPro.DeviceSupport; +//using PepperDash.Core; +//using PepperDash.Essentials.Core; +//using PepperDash.Essentials.Core.SmartObjects; +//using PepperDash.Essentials.Core.PageManagers; + +//namespace PepperDash.Essentials +//{ +// public class DualDisplaySimpleOrAdvancedRouting : PanelDriverBase +// { +// EssentialsPresentationPanelAvFunctionsDriver Parent; + +// /// +// /// Smart Object 3200 +// /// +// SubpageReferenceList SourcesSrl; + +// /// +// /// For tracking feedback on last selected +// /// +// BoolInputSig LastSelectedSourceSig; + +// /// +// /// The source that has been selected and is awaiting assignment to a display +// /// +// SourceListItem PendingSource; + +// bool IsSharingModeAdvanced; + +// public DualDisplaySimpleOrAdvancedRouting(EssentialsPresentationPanelAvFunctionsDriver parent) : base(parent.TriList) +// { +// Parent = parent; +// SourcesSrl = new SubpageReferenceList(TriList, 3200, 3, 3, 3); + +// TriList.SetSigFalseAction(UIBoolJoin.ToggleSharingModePress, ToggleSharingModePressed); + +// TriList.SetSigFalseAction(UIBoolJoin.Display1AudioButtonPressAndFb, Display1AudioPress); +// TriList.SetSigFalseAction(UIBoolJoin.Display1ControlButtonPress, Display1ControlPress); +// TriList.SetSigTrueAction(UIBoolJoin.Display1SelectPressAndFb, Display1Press); + +// TriList.SetSigFalseAction(UIBoolJoin.Display2AudioButtonPressAndFb, Display2AudioPress); +// TriList.SetSigFalseAction(UIBoolJoin.Display2ControlButtonPress, Display2ControlPress); +// TriList.SetSigTrueAction(UIBoolJoin.Display2SelectPressAndFb, Display2Press); +// } + +// /// +// /// +// /// +// public override void Show() +// { +// TriList.BooleanInput[UIBoolJoin.ToggleSharingModeVisible].BoolValue = true; +// TriList.BooleanInput[UIBoolJoin.StagingPageVisible].BoolValue = true; +// if(IsSharingModeAdvanced) +// TriList.BooleanInput[UIBoolJoin.DualDisplayPageVisible].BoolValue = true; +// else +// TriList.BooleanInput[UIBoolJoin.SelectASourceVisible].BoolValue = true; +// base.Show(); +// } + +// /// +// /// +// /// +// //public override void Hide() +// //{ +// // TriList.BooleanInput[UIBoolJoin.ToggleSharingModeVisible].BoolValue = false; +// // TriList.BooleanInput[UIBoolJoin.StagingPageVisible].BoolValue = false; +// // if(IsSharingModeAdvanced) +// // TriList.BooleanInput[UIBoolJoin.DualDisplayPageVisible].BoolValue = false; +// // else +// // TriList.BooleanInput[UIBoolJoin.SelectASourceVisible].BoolValue = false; +// // base.Hide(); +// //} + +// public void SetCurrentRoomFromParent() +// { +// if (IsSharingModeAdvanced) +// return; // add stuff here +// else +// SetupSourceListForSimpleRouting(); +// } + +// /// +// /// +// /// +// void SetupSourceListForSimpleRouting() +// { +// // get the source list config and set up the source list +// var config = ConfigReader.ConfigObject.SourceLists; +// if (config.ContainsKey(Parent.CurrentRoom.SourceListKey)) +// { +// var srcList = config[Parent.CurrentRoom.SourceListKey] +// .Values.ToList().OrderBy(s => s.Order); +// // Setup sources list +// uint i = 1; // counter for UI list +// foreach (var srcConfig in srcList) +// { +// if (!srcConfig.IncludeInSourceList) // Skip sources marked this way +// continue; + +// var sourceKey = srcConfig.SourceKey; +// var actualSource = DeviceManager.GetDeviceForKey(sourceKey) as Device; +// if (actualSource == null) +// { +// Debug.Console(0, "Cannot assign missing source '{0}' to source UI list", +// srcConfig.SourceKey); +// continue; +// } +// var localSrcItem = srcConfig; // lambda scope below +// var localIndex = i; +// SourcesSrl.GetBoolFeedbackSig(i, 1).UserObject = new Action(b => +// { +// if (IsSharingModeAdvanced) +// { +// if (LastSelectedSourceSig != null) +// LastSelectedSourceSig.BoolValue = false; +// SourceListButtonPress(localSrcItem); +// LastSelectedSourceSig = SourcesSrl.BoolInputSig(localIndex, 1); +// LastSelectedSourceSig.BoolValue = true; +// } +// else +// Parent.CurrentRoom.DoSourceToAllDestinationsRoute(localSrcItem); +// }); +// SourcesSrl.StringInputSig(i, 1).StringValue = srcConfig.PreferredName; +// i++; + +// //var item = new SubpageReferenceListSourceItem(i++, SourcesSrl, srcConfig, +// // b => { if (!b) UiSelectSource(localSrcConfig); }); +// //SourcesSrl.AddItem(item); // add to the SRL +// //item.RegisterForSourceChange(Parent.CurrentRoom); +// } +// SourcesSrl.Count = (ushort)(i - 1); +// Parent.CurrentRoom.CurrentSingleSourceChange += CurrentRoom_CurrentSourceInfoChange; +// Parent.CurrentRoom.CurrentDisplay1SourceChange += CurrentRoom_CurrentDisplay1SourceChange; +// Parent.CurrentRoom.CurrentDisplay2SourceChange += CurrentRoom_CurrentDisplay2SourceChange; +// } +// } + +// void SetupSourceListForAdvancedRouting() +// { + +// } + +// void CurrentRoom_CurrentSourceInfoChange(EssentialsRoomBase room, SourceListItem info, ChangeType type) +// { + +// } + +// void CurrentRoom_CurrentDisplay1SourceChange(EssentialsRoomBase room, SourceListItem info, ChangeType type) +// { +// TriList.StringInput[UIStringJoin.Display1SourceLabel].StringValue = PendingSource.PreferredName; + +// } + +// void CurrentRoom_CurrentDisplay2SourceChange(EssentialsRoomBase room, SourceListItem info, ChangeType type) +// { +// TriList.StringInput[UIStringJoin.Display2SourceLabel].StringValue = PendingSource.PreferredName; +// } + +// /// +// /// +// /// +// void ToggleSharingModePressed() +// { +// Hide(); +// IsSharingModeAdvanced = !IsSharingModeAdvanced; +// TriList.BooleanInput[UIBoolJoin.ToggleSharingModePress].BoolValue = IsSharingModeAdvanced; +// Show(); +// } + +// public void SourceListButtonPress(SourceListItem item) +// { +// // start the timer +// // show FB on potential source +// TriList.BooleanInput[UIBoolJoin.Display1AudioButtonEnable].BoolValue = false; +// TriList.BooleanInput[UIBoolJoin.Display1ControlButtonEnable].BoolValue = false; +// TriList.BooleanInput[UIBoolJoin.Display2AudioButtonEnable].BoolValue = false; +// TriList.BooleanInput[UIBoolJoin.Display2ControlButtonEnable].BoolValue = false; +// PendingSource = item; +// } + +// void EnableAppropriateDisplayButtons() +// { +// TriList.BooleanInput[UIBoolJoin.Display1AudioButtonEnable].BoolValue = true; +// TriList.BooleanInput[UIBoolJoin.Display1ControlButtonEnable].BoolValue = true; +// TriList.BooleanInput[UIBoolJoin.Display2AudioButtonEnable].BoolValue = true; +// TriList.BooleanInput[UIBoolJoin.Display2ControlButtonEnable].BoolValue = true; +// if (LastSelectedSourceSig != null) +// LastSelectedSourceSig.BoolValue = false; +// } + +// public void Display1Press() +// { +// EnableAppropriateDisplayButtons(); +// Parent.CurrentRoom.SourceToDisplay1(PendingSource); +// // Enable end meeting +// } + +// public void Display1AudioPress() +// { + +// } + + +// public void Display1ControlPress() +// { + +// } + +// public void Display2Press() +// { +// EnableAppropriateDisplayButtons(); +// Parent.CurrentRoom.SourceToDisplay2(PendingSource); +// } + +// public void Display2AudioPress() +// { + +// } + +// public void Display2ControlPress() +// { + +// } +// } +//} \ No newline at end of file diff --git a/essentials-framework/references/PepperDash_Core.dll b/essentials-framework/references/PepperDash_Core.dll new file mode 100644 index 0000000000000000000000000000000000000000..428b92496261ca1ee02ee9f05dcfc7fd2947e73e GIT binary patch literal 142520 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 H#h?ENY1_=X literal 0 HcmV?d00001

XWu3yC?E&D*D-opA$fcg+i-jlcb#Jgsu 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�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%0D3`5oYYt@>69i%L(`U^qTWBxZLRQ>DG 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 Ziw$xG8EBY4Y$1c5#-P^G4QTR#{U5*s_i6wD literal 0 HcmV?d00001 diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/SimplSharpData.dat b/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/SimplSharpData.dat new file mode 100644 index 0000000000000000000000000000000000000000..816bfe12a1cc8843e7a36cb83b974bc9598bbd95 GIT binary patch literal 2016 zcmV<62Os!Ab6Iat?7tDe_glwHG}?u}Ox7g!_W1$4ugh(PzP;JNl+_f0#EM?6aXVfy zn>qV?Q9T`KA@j5FR-I)H0mEmY%ipp7Oq129Ei=<>MXsQmfb9uUVS|f@^`^!+)L(tE z-iex0=JbsvCU*Ks_ZxJI!R;n4us9K^kugL*$nZ<=!J_bFQp^l$B6g&zH4OSrUv3>H z_1P+Il{=->z1XC4Tk_CU+4NIuD-QdJ@4UX`KhNtrNKE<1v;7`IZ0S0>$()0Mpj%L3 zWI;6sP>t=;>G5r+F$<}Cfj}4T<#NIyE2u1KQhs9C)wB{}by@7J zJRM<|i8KQ=M*^j#4fDZV*12$N%iP#rB$9OU9I{Wi3?N?_qt}AWNU=hszjg{F^sDOo za-L+}@1 zdE1I0#WO~qCGvpYQ87Eef8}J>9Fo^w^PG~4h@w*C|6YDb-goWqD5@jAii*ErPyUGx zZzN4+>(k>4xDO6)Uc{w-@x+0_c7@*!>;QH{9})*!q%eNyvPP(_xdK|1ArySVE0!*( z@#2C$BxQxzAGTojKTMOahesuz>A*(nn08P+j?9d14fCIo5?r-K`w4 zL8wJGY>71FS>NyBPtxQ= zKJ+iA@pSVjRTLr2eu&$k+2E&jHOl^YkXvFDbG3j32k1UN>AaB~_mkXKO1(%4^}*&C z78!R7n6R+&;mFl;NA$#$z>16`0yD*do^f{@06Agk1;|eJn-|i3zyfrZmduVP1#P1d zZ@7|S@$N$=kqO-VOEmMbG|)1V4&l&?raN)wXGU{mJfw+%Ooa9aD6~?YUsx83IKC~i z_z+63S-2UT0TPm)A$raWkm-W9oi3eheeP#?7-z&2AmYLg1T%*Wsi|m#cyR`_rQIxZgi4XrIj5u!VviuS>!lFJBdy{jG*Ij z*}YjKpicfy)e7n$w2ui#PBLhp%4E@VU9)jqC;CFo8VP+E%}q-uLbq%7o_RJ2swU&? zp)Rl7!2J?U%a^Ntk$yJ}pw{bBBH~ut1{ylPBUvUTTCDzjv8S@)sWtS*Z`};f$M)yT znAzajtOp+x#$ilxNNB9y(AT3S<*R~jeybYOM*9C`OCgdbFR)w~?cVQx0Ik-^KMTar zwiZ(1g0Z_1Q0V&1U-l(}pHRaicB$5Q^U)kA?x&v-Bp7W4@5wtf+Gh);hF(o08Bph< zCSvem!XG@&SAkC<1>?%hNuO{LVgzo-abf zu-+*FUl5ItHC>jW{U$yR2(E}4I{oX#K^oVBxN5URk>8r@J>b!vo$=weNTCbZ`7#+; zY_dn)AV!A-7S2Cu>JjkCz0t)6q77ZfNEG}qbi}?h_}SO zTWWM|g@5Oeip71djRRGgHK$HPO{#4OMGlB3ZJm$l%XKe=sUm=Nv-4-r#t~)7WH%SF zZr3RxqHXR2EoiC(K)|OrQ1m5&Abv0i+_*#0LP2!&+6v*_%lI*J==ga{!!CKH$=ZsL7fh8F&JN!%n*6TNQ*STV(tr8bgQ{)Vei_%u*$x*)@bc8!gKRwlq^RxawFMgAwnop?o}8bPlcBthPonjQ zran2VFyHQbW%uf^Ma|Igkd(iJm7>U)z5c2>=A^zwwVfz0mV?Rz+X!jC4*g70<^(xA zBUJy2$!Ml<5Imb^|D71fIWGK_gp0$L@5v%7ze4vYkzMf1Ac|!9WYFEn_caHBUs_eufr=T)IH` y;wdgdaLtpd5tb%1AV|#v*mrh(EJP+kAPh>F>f$)0%$BB@v%&z~0>1`IB7?)Vu;x4f literal 0 HcmV?d00001 diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/manifest.info b/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/manifest.info new file mode 100644 index 00000000..821d5130 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/manifest.info @@ -0,0 +1,30 @@ +MainAssembly=SSMonoProTaskLibrary.dll:5d3a301400516bd812bf1566c72ccbf2 +MainAssemblyMinFirmwareVersion=1.009.0029 +ü +DependencySource=SimplSharpReflectionInterface.dll:e3ff8edbba84ccd7155b9984e67488b2 +DependencyPath=SSMonoProTaskLibrary.cplz:SimplSharpReflectionInterface.dll +DependencyMainAssembly=SimplSharpReflectionInterface.dll:e3ff8edbba84ccd7155b9984e67488b2 +ü +DependencySource=SSharpCrestronExtensionsLibrary.dll:776d0247d8d42164c46c7cc1dfadbd03 +DependencyPath=SSMonoProTaskLibrary.cplz:SSharpCrestronExtensionsLibrary.dll +DependencyMainAssembly=SSharpCrestronExtensionsLibrary.dll:776d0247d8d42164c46c7cc1dfadbd03 +ü +DependencySource=SSMonoConcurrentCollectionsLibrary.dll:b0afcd989b081899c9eb29f9e4c8b799 +DependencyPath=SSMonoProTaskLibrary.cplz:SSMonoConcurrentCollectionsLibrary.dll +DependencyMainAssembly=SSMonoConcurrentCollectionsLibrary.dll:b0afcd989b081899c9eb29f9e4c8b799 +ü +DependencySource=SSMonoProConcurrentCollectionsLibrary.dll:8b718ce29f938bbf9cb5b8fc2d89332f +DependencyPath=SSMonoProTaskLibrary.cplz:SSMonoProConcurrentCollectionsLibrary.dll +DependencyMainAssembly=SSMonoProConcurrentCollectionsLibrary.dll:8b718ce29f938bbf9cb5b8fc2d89332f +ü +DependencySource=SSMonoSupportLibrary.dll:59362515f2c1d61583b2e40793987917 +DependencyPath=SSMonoProTaskLibrary.cplz:SSMonoSupportLibrary.dll +DependencyMainAssembly=SSMonoSupportLibrary.dll:59362515f2c1d61583b2e40793987917 +ü +DependencySource=SSMonoThreadingLibrary.dll:ea2ae2e1d9c425236f39de9192591062 +DependencyPath=SSMonoProTaskLibrary.cplz:SSMonoThreadingLibrary.dll +DependencyMainAssembly=SSMonoThreadingLibrary.dll:ea2ae2e1d9c425236f39de9192591062 +ü +DependencySource=SSMonoTupleLibrary.dll:2a3b419fff4199838079879053fcb41d +DependencyPath=SSMonoProTaskLibrary.cplz:SSMonoTupleLibrary.dll +DependencyMainAssembly=SSMonoTupleLibrary.dll:2a3b419fff4199838079879053fcb41d diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/manifest.ser b/essentials-framework/Essentials Core/PepperDashEssentialsBase/References/SSMonoProTaskLibrary.cplz/manifest.ser new file mode 100644 index 0000000000000000000000000000000000000000..d24ab36c3b8a0cb74b817db2cc88c72dafc2d68c GIT binary patch literal 914 zcmV;D18w{tiwFP!000040PVd8U>r%7B_=Oz%6orG^-9;^bfE}=m|e(^`YTQU3S(MP)g(}z zRsMjQe32E)$YX)OIJ2e&64hTK#lBZJXcs_dTiermd^x@B90{ zE*7iu$5Z+$mA8wWzK=m2`gBqdm{)n3XH6b&qojM9ukz(AUrwouj~;!`(EeVB_CVK-KO8jR9)2B;%U}q!E8PKy>!ZMfY+h*_eUSS${QXV|M^QshWh(m z&Ier6;pw|JUxMA6a3&pn9z6JTwzIeYV0JLu-`o9kf4WbMJe}^&&a>HMwsS9_|E*wm zXW$k++&lxf>iIpyB@mU3uy4_zD?qow_15!yKc?AEs^xTD*ZHzNsY>Ob!6}~XWarb_ z!-I#DorB$jhYzO@^U2=B^N0EV^kDMf;lpr>ZwbCT%eUy{7PEZI&cE(N-vs`4ZDRWJ z7D(TE#y=gfWw+?V%?A;E_h6c13ho_DCg%^Qllzl{^XcB~;NiV{d*@Lrb&DRl^L~qd zZaMF_>;Gqa>Tkee&|2OC^;!-W-5AIF& z^8K9$5AQuZc<^xdL4UrZ0K3zB3$VAG-e`9pGMgd5_{2mzz4bgC#OC46yw0;(vAh_X zi9FlO^1Xa__HeqtcYp8Rr|0(`&hm%55BKgr+}-&!;vWux?71>G1HR>440QM*v(W`P z{CRAi1`xXSOg)Uw)OuCsL$kD(-J9(1K0H4^r+*(F+&dtiCXU{@fA4%sU(6!2)Cbp{ op<4jG?F{uh`=HtJz{Nj{&PpGNn`Wf Feedbacks + { + get + { + return new FeedbackCollection + { + RoomIsOnFeedback, + IsCoolingDownFeedback, + IsWarmingUpFeedback + }; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/RoomCues.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/RoomCues.cs new file mode 100644 index 00000000..0d439d09 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/RoomCues.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; + +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials +{ + public static class RoomCue + { + // Commands/ + //bools + public static readonly Cue RoomOnToggle = Cue.BoolCue("RoomOnToggle", 2001); + public static readonly Cue RoomOn = Cue.BoolCue("RoomOn", 2002); + public static readonly Cue RoomOff = Cue.BoolCue("RoomOff", 2003); + public static readonly Cue VolumeUp = Cue.BoolCue("VolumeUp", 2011); + public static readonly Cue VolumeDown = Cue.BoolCue("VolumeDown", 2012); + public static readonly Cue VolumeDefault = Cue.BoolCue("VolumeDefault", 2013); + public static readonly Cue MuteToggle = Cue.BoolCue("MuteToggle", 2014); + public static readonly Cue MuteOn = Cue.BoolCue("MuteOn", 2015); + public static readonly Cue MuteOff = Cue.BoolCue("MuteOff", 2016); + + //ushorts + public static readonly Cue SelectSourceByNumber = Cue.UShortCue("SelectSourceByNumber", 2001); + public static readonly Cue VolumeLevel = Cue.UShortCue("VolumeLevel", 2011); + public static readonly Cue VolumeLevelPercent = Cue.UShortCue("VolumeLevelPercent", 2012); + + //strings + public static readonly Cue SelectSourceByKey = Cue.StringCue("SelectSourceByKey", 2001); + + // Outputs + //Bools + public static readonly Cue RoomIsOn = Cue.BoolCue("RoomIsOn", 2002); + public static readonly Cue RoomIsOnStandby = Cue.BoolCue("RoomIsOnStandby", 2003); + public static readonly Cue RoomIsWarmingUp = Cue.BoolCue("RoomIsWarmingUp", 2004); + public static readonly Cue RoomIsCoolingDown = Cue.BoolCue("RoomIsCoolingDown", 2005); + public static readonly Cue RoomIsOccupied = Cue.BoolCue("RoomIsOccupied", 2006); + + //Ushorts + public static readonly Cue SourcesCount = Cue.UShortCue("SourcesCount", 2001); + public static readonly Cue CurrentSourceNumber = Cue.UShortCue("CurrentSourceNumber", 2002); + public static readonly Cue CurrentSourceType = Cue.UShortCue("CurrentSourceType", 2003); + + //Strings + public static readonly Cue CurrentSourceKey = Cue.StringCue("CurrentSourceKey", 2001); + public static readonly Cue CurrentSourceName = Cue.StringCue("CurrentSourceName", 2002); + + public static readonly Cue VolumeLevelText = Cue.StringCue("VolumeLevelText", 2012); + + public static readonly Cue Key = Cue.StringCue("RoomKey", 2021); + public static readonly Cue Name = Cue.StringCue("RoomName", 2022); + public static readonly Cue Description = Cue.StringCue("Description", 2023); + public static readonly Cue HelpMessage = Cue.StringCue("HelpMessage", 2024); + + //Special + public static readonly Cue Source = new Cue("Source", 0, eCueType.Other); + } +} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/IRoutingInputsExtensions.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/IRoutingInputsExtensions.cs new file mode 100644 index 00000000..ad4a3458 --- /dev/null +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Routing-CHECK REMOVE/IRoutingInputsExtensions.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DM; + +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + ///